Added some React IDs for playwright e2e tests (#13265)

### What problem does this PR solve?

Necessary ids for implementing the new testing suite with playwright for
UI

### Type of change

- [x] Other (please describe): Testing IDs

Co-authored-by: Liu An <asiro@qq.com>
This commit is contained in:
Idriss Sbaaoui
2026-02-28 15:13:47 +08:00
committed by GitHub
parent 1027916bfe
commit e62552d482
37 changed files with 365 additions and 218 deletions

View File

@@ -79,8 +79,9 @@ export const EmptyAppCard = (props: {
isSearch?: boolean;
size?: 'small' | 'large';
children?: React.ReactNode;
testId?: string;
}) => {
const { type, showIcon, className, isSearch, children } = props;
const { type, showIcon, className, isSearch, children, testId } = props;
let defaultClass = '';
let style = {};
switch (props.size) {
@@ -97,7 +98,7 @@ export const EmptyAppCard = (props: {
break;
}
return (
<div onClick={isSearch ? undefined : props.onClick}>
<div onClick={isSearch ? undefined : props.onClick} data-testid={testId}>
<EmptyCard
icon={showIcon ? EmptyCardData[type].icon : undefined}
title={

View File

@@ -65,6 +65,7 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
>
{(field) => (
<Switch
data-testid="parse-on-creation-toggle"
onCheckedChange={field.onChange}
checked={field.value}
></Switch>
@@ -77,6 +78,7 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
value={field.value}
onValueChange={field.onChange}
accept={{ '*': [] }}
data-testid="dataset-upload-dropzone"
/>
)}
</RAGFlowFormItem>
@@ -97,7 +99,7 @@ export function FileUploadDialog({
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogContent data-testid="dataset-upload-modal">
<DialogHeader>
<DialogTitle>{t('fileManager.uploadFile')}</DialogTitle>
</DialogHeader>

View File

@@ -14,6 +14,7 @@ interface IProps {
moreDropdown: React.ReactNode;
sharedBadge?: ReactNode;
icon?: React.ReactNode;
testId?: string;
}
export function HomeCard({
data,
@@ -21,9 +22,12 @@ export function HomeCard({
moreDropdown,
sharedBadge,
icon,
testId,
}: IProps) {
return (
<Card
data-testid={testId}
data-agent-name={data.name}
onClick={() => {
// navigateToSearch(data?.id);
onClick?.();
@@ -40,7 +44,7 @@ export function HomeCard({
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
<section className="flex justify-between">
<section className="flex flex-1 min-w-0 gap-1 items-center">
<div className="text-base font-bold leading-snug truncate">
<div className="text-base font-bold leading-snug truncate" data-testid="agent-name">
{data.name}
</div>
{icon}

View File

@@ -122,6 +122,7 @@ export function KnowledgeBaseFormField({
</FormLabel>
<FormControl>
<MultiSelect
data-testid="chat-datasets-combobox"
options={options}
onValueChange={(value) => {
handleDatasetSelectChange(value, field.onChange);
@@ -131,6 +132,8 @@ export function KnowledgeBaseFormField({
maxCount={100}
defaultValue={field.value}
showSelectAll={false}
popoverTestId="datasets-options"
optionTestIdPrefix="datasets"
{...field}
/>
</FormControl>

View File

@@ -211,6 +211,7 @@ export function NextMessageInput({
</FileUploadList>
<Textarea
data-testid="chat-textarea"
value={value}
onChange={onInputChange}
placeholder={t('chat.messagePlaceholder')}
@@ -267,7 +268,7 @@ export function NextMessageInput({
</div>
{sendLoading ? (
<Button onClick={stopOutputMessage} size="icon-xs">
<Button data-testid="chat-stream-status" onClick={stopOutputMessage} size="icon-xs">
<CircleStop />
</Button>
) : (

View File

@@ -48,6 +48,7 @@ export type SelectWithSearchFlagProps = {
disabled?: boolean;
placeholder?: string;
emptyData?: string;
testId?: string;
};
function findLabelWithoutOptions(
@@ -80,6 +81,7 @@ export const SelectWithSearch = forwardRef<
disabled = false,
placeholder = t('common.selectPlaceholder'),
emptyData = t('common.noDataFound'),
testId,
},
ref,
) => {
@@ -154,6 +156,7 @@ export const SelectWithSearch = forwardRef<
aria-expanded={open}
ref={ref}
disabled={disabled}
data-testid={testId}
className={cn(
'!bg-bg-input hover:bg-background border-border-button w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto group',
triggerClassName,
@@ -213,6 +216,7 @@ export const SelectWithSearch = forwardRef<
value={option.value}
disabled={option.disabled}
onSelect={handleSelect}
data-testid="combobox-option"
className={
value === option.value ? 'bg-bg-card' : ''
}
@@ -234,6 +238,7 @@ export const SelectWithSearch = forwardRef<
value={group.value}
disabled={group.disabled}
onSelect={handleSelect}
data-testid="combobox-option"
className={cn('mb-1 min-h-10 ', {
'bg-bg-card ': value === group.value,
})}

View File

@@ -23,7 +23,7 @@ export function RenameDialog({
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogContent className="sm:max-w-[425px]" data-testid="rename-modal">
<DialogHeader>
<DialogTitle>{title || t('common.rename')}</DialogTitle>
</DialogHeader>
@@ -33,7 +33,7 @@ export function RenameDialog({
onOk={onOk}
></RenameForm>
<DialogFooter>
<ButtonLoading type="submit" form={TagRenameId} loading={loading}>
<ButtonLoading data-testid="rename-save" type="submit" form={TagRenameId} loading={loading}>
{t('common.save')}
</ButtonLoading>
</DialogFooter>

View File

@@ -66,6 +66,7 @@ export function RenameForm({
<FormLabel>{t('common.name')}</FormLabel>
<FormControl>
<Input
data-testid="rename-name-input"
placeholder={t('common.namePlaceholder')}
{...field}
autoComplete="off"

View File

@@ -33,6 +33,8 @@ export interface ModalProps {
onCancel?: () => void;
okButtonClassName?: string;
cancelButtonClassName?: string;
testId?: string;
okButtonTestId?: string;
disabled?: boolean;
style?: React.CSSProperties;
zIndex?: number;
@@ -87,6 +89,8 @@ const Modal: ModalType = ({
style,
zIndex = 50,
type,
testId,
okButtonTestId,
}) => {
const sizeClasses = {
small: 'max-w-md',
@@ -152,6 +156,7 @@ const Modal: ModalType = ({
type="button"
disabled={confirmLoading || disabled}
onClick={() => handleOk()}
data-testid={okButtonTestId}
className={cn(
'px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
{ 'cursor-not-allowed': disabled },
@@ -190,6 +195,7 @@ const Modal: ModalType = ({
okButtonClassName,
cancelButtonClassName,
showCancel,
okButtonTestId,
]);
const contentEl = useMemo(() => {
@@ -224,6 +230,7 @@ const Modal: ModalType = ({
`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-bg-base rounded-lg shadow-lg border border-border-default transition-all focus-visible:!outline-none`,
{ 'pt-10': closable && !title && !type },
)}
data-testid={testId}
style={style}
onClick={(e) => e.stopPropagation()}
>

View File

@@ -49,10 +49,12 @@ function MultiCommandItem({
option,
isSelected,
toggleOption,
optionTestId,
}: {
option: MultiSelectOptionType;
isSelected: boolean;
toggleOption(value: string): void;
optionTestId?: string;
}) {
return (
<CommandItem
@@ -61,6 +63,7 @@ function MultiCommandItem({
if (option.disabled) return false;
toggleOption(option.value);
}}
data-testid={optionTestId}
className={cn('cursor-pointer', {
'cursor-not-allowed text-text-disabled': option.disabled,
})}
@@ -183,6 +186,8 @@ interface MultiSelectProps
* If true, renders the multi-select component with a select all option.
*/
showSelectAll?: boolean;
popoverTestId?: string;
optionTestIdPrefix?: string;
}
export const MultiSelect = React.forwardRef<
@@ -202,6 +207,8 @@ export const MultiSelect = React.forwardRef<
// asChild = false,
className,
showSelectAll = true,
popoverTestId,
optionTestIdPrefix,
...props
},
ref,
@@ -378,6 +385,7 @@ export const MultiSelect = React.forwardRef<
className="w-auto p-0"
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}
data-testid={popoverTestId}
>
<Command className="p-5 pb-8">
{options && options.length > 0 && (
@@ -410,7 +418,7 @@ export const MultiSelect = React.forwardRef<
)}
{!options.some((x) => 'options' in x) &&
(options as unknown as MultiSelectOptionType[]).map(
(option) => {
(option, idx) => {
const isSelected = selectedValues.includes(option.value);
return (
<MultiCommandItem
@@ -418,6 +426,7 @@ export const MultiSelect = React.forwardRef<
key={option.value}
isSelected={isSelected}
toggleOption={toggleOption}
optionTestId={optionTestIdPrefix ? `${optionTestIdPrefix}-option-${idx}` : undefined}
></MultiCommandItem>
);
},
@@ -426,7 +435,7 @@ export const MultiSelect = React.forwardRef<
{options.every((x) => 'options' in x) &&
options.map((x, idx) => (
<CommandGroup heading={x.label} key={idx}>
{x.options.map((option) => {
{x.options.map((option, optIdx) => {
const isSelected = selectedValues.includes(option.value);
return (
@@ -435,6 +444,7 @@ export const MultiSelect = React.forwardRef<
key={option.value}
isSelected={isSelected}
toggleOption={toggleOption}
optionTestId={optionTestIdPrefix ? `${optionTestIdPrefix}-option-${optIdx}` : undefined}
></MultiCommandItem>
);
})}

View File

@@ -94,7 +94,19 @@ export function Header() {
tag.path === Routes.Root ? (
<HeaderIcon className="size-6"></HeaderIcon>
) : (
<span>{tag.name}</span>
<span
data-testid={
tag.path === Routes.Chats
? 'nav-chat'
: tag.path === Routes.Searches
? 'nav-search'
: tag.path === Routes.Agents
? 'nav-agent'
: undefined
}
>
{tag.name}
</span>
),
value: tag.path,
};
@@ -128,7 +140,7 @@ export function Header() {
}, [pathname]);
return (
<section className="py-5 px-10 flex justify-between items-center ">
<section className="py-5 px-10 flex justify-between items-center " data-testid="top-nav">
<div className="flex items-center gap-4">
<img
src={'/logo.svg'}
@@ -146,7 +158,7 @@ export function Header() {
onChange={handleChange}
activeClassName="text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2"
></Segmented>
<div className="flex items-center gap-5 text-text-badge">
<div className="flex items-center gap-5 text-text-badge" data-testid="auth-status">
<a
target="_blank"
href="https://discord.com/invite/NjYzJD3GM3"
@@ -183,7 +195,7 @@ export function Header() {
{theme === 'light' ? <Sun /> : <Moon />}
</Button>
<BellButton></BellButton>
<div className="relative">
<div className="relative" data-testid="settings-entrypoint">
<RAGFlowAvatar
name={nickname}
avatar={avatar}

View File

@@ -71,6 +71,7 @@ function AgentChatBox() {
<section className="flex flex-1 flex-col px-5 min-h-0 pb-4">
<div className="flex-1 overflow-auto" ref={messageContainerRef}>
<div>
{!sendLoading && <div data-testid="agent-run-idle" />}
{/* <Spin spinning={sendLoading}> */}
{derivedMessages?.map((message, i) => {
return (

View File

@@ -12,6 +12,7 @@ export function ChatSheet({ hideModal }: IModalProps<any>) {
return (
<Sheet open modal={false} onOpenChange={hideModal}>
<SheetContent
data-testid="agent-run-chat"
className={cn('top-20 bottom-0 p-0 flex flex-col h-auto')}
onInteractOutside={(e) => e.preventDefault()}
>

View File

@@ -210,7 +210,7 @@ export default function Agent() {
} = useRunDataflow({ showLogSheet: showPipelineLogSheet, setMessageId });
return (
<section className="h-full">
<section className="h-full" data-testid="agent-detail">
<PageHeader>
<section>
<Breadcrumb>
@@ -245,7 +245,7 @@ export default function Agent() {
>
<MessageSquareCode /> {t('flow.conversationVariable')}
</ButtonLoading>
<Button variant={'secondary'} onClick={handleButtonRunClick}>
<Button data-testid="agent-run" variant={'secondary'} onClick={handleButtonRunClick}>
<CirclePlay />
{t('flow.run')}
</Button>

View File

@@ -18,6 +18,7 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
return (
<HomeCard
testId="agent-card"
data={{ ...data, name: data.title, description: data.description || '' }}
moreDropdown={
<AgentDropdown showAgentRenameModal={showAgentRenameModal} agent={data}>

View File

@@ -22,7 +22,7 @@ export function CreateAgentDialog({
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogContent data-testid="agent-create-modal">
<DialogHeader>
<DialogTitle>{t('flow.createGraph')}</DialogTitle>
</DialogHeader>
@@ -32,7 +32,7 @@ export function CreateAgentDialog({
shouldChooseAgent={shouldChooseAgent}
></CreateAgentForm>
<DialogFooter>
<ButtonLoading type="submit" form={TagRenameId} loading={loading}>
<ButtonLoading data-testid="agent-save" type="submit" form={TagRenameId} loading={loading}>
{t('common.save')}
</ButtonLoading>
</DialogFooter>

View File

@@ -93,6 +93,7 @@ export default function Agents() {
>
<div className="flex flex-col gap-y-5 text-text-secondary text-sm pt-5">
<div
data-testid="agents-empty-create"
className="flex items-center gap-x-2 hover:text-text-primary cursor-pointer"
onClick={showCreatingModal}
>
@@ -117,7 +118,7 @@ export default function Agents() {
</EmptyAppCard>
</div>
)}
<section className="flex flex-col w-full flex-1">
<section className="flex flex-col w-full flex-1" data-testid="agents-list">
{(!!data?.length || searchString) && (
<>
<div className="px-8 pt-8 ">
@@ -131,13 +132,13 @@ export default function Agents() {
value={filterValue}
>
<DropdownMenu>
<DropdownMenuTrigger>
<DropdownMenuTrigger data-testid="create-agent">
<Button>
<Plus className="h-4 w-4" />
{t('flow.createGraph')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuContent data-testid="agent-create-menu">
<DropdownMenuItem
justifyBetween={false}
onClick={showCreatingModal}
@@ -153,6 +154,7 @@ export default function Agents() {
{t('flow.createFromTemplate')}
</DropdownMenuItem>
<DropdownMenuItem
data-testid="agent-import-json"
justifyBetween={false}
onClick={handleImportJson}
>

View File

@@ -17,7 +17,7 @@ export function NameFormField() {
const { t } = useTranslation();
return (
<RAGFlowFormItem name="name" required label={t('common.name')}>
<Input placeholder={t('common.namePlaceholder')} autoComplete="off" />
<Input data-testid="agent-name-input" placeholder={t('common.namePlaceholder')} autoComplete="off" />
</RAGFlowFormItem>
);
}

View File

@@ -20,13 +20,13 @@ export function UploadAgentDialog({
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogContent data-testid="agent-import-modal">
<DialogHeader>
<DialogTitle>{t('fileManager.uploadFile')}</DialogTitle>
</DialogHeader>
<UploadAgentForm hideModal={hideModal} onOk={onOk}></UploadAgentForm>
<DialogFooter>
<ButtonLoading type="submit" form={TagRenameId} loading={loading}>
<ButtonLoading data-testid="agent-import-save" type="submit" form={TagRenameId} loading={loading}>
{t('common.save')}
</ButtonLoading>
</DialogFooter>

View File

@@ -53,6 +53,7 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) {
<FormLabel required>DSL</FormLabel>
<FormControl>
<FileUploader
data-testid="agent-import-file"
className="text-ellipsis overflow-hidden"
value={field.value}
onValueChange={field.onChange}

View File

@@ -105,6 +105,7 @@ export function DatasetActionCell({
)}
<ConfirmDeleteDialog onOk={handleRemove}>
<Button
data-testid="document-delete"
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}

View File

@@ -151,6 +151,8 @@ export function DatasetTable({
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-testid="document-row"
data-doc-name={row.original.name}
data-state={row.getIsSelected() && 'selected'}
className="group"
>

View File

@@ -40,6 +40,15 @@ const IconMap = {
),
};
const ParseStatusStateMap = {
[RunningStatus.UNSTART]: 'unstart',
[RunningStatus.RUNNING]: 'running',
[RunningStatus.CANCEL]: 'cancel',
[RunningStatus.DONE]: 'success',
[RunningStatus.FAIL]: 'fail',
[RunningStatus.SCHEDULE]: 'running',
} as const;
export function ParsingStatusCell({
record,
showChangeParserModal,
@@ -89,7 +98,11 @@ export function ParsingStatusCell({
showLog(record);
};
return (
<section className="flex gap-8 items-center">
<section
className="flex gap-8 items-center"
data-testid="document-parse-status"
data-state={ParseStatusStateMap[run] ?? 'unknown'}
>
<div className="text-ellipsis w-[100px] flex items-center justify-between">
<DropdownMenu>
<DropdownMenuTrigger>

View File

@@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react';
import React, { createContext, useEffect, useState } from 'react';
import './index.less';
export const FlipFaceContext = createContext<'front' | 'back'>('front');
type IProps = {
children: React.ReactNode;
isLoginPage: boolean;
@@ -32,18 +34,34 @@ const FlipCard3D = (props: IProps) => {
className={`relative w-full h-full transition-transform transform-style-3d ${isFlipped ? 'rotate-y-180' : ''}`}
>
{/* Front Face */}
<div className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-0">
{children}
<div
className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-0"
{...(!isFlipped ? { 'data-testid': 'auth-card-active' } : {})}
>
<FlipFaceContext.Provider value="front">
{children}
</FlipFaceContext.Provider>
</div>
{/* Back Face */}
<div className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-180">
{children}
<div
className="absolute inset-0 flex items-center justify-center backface-hidden rotate-y-180"
{...(isFlipped ? { 'data-testid': 'auth-card-active' } : {})}
>
<FlipFaceContext.Provider value="back">
{children}
</FlipFaceContext.Provider>
</div>
</div>
</div>
)}
{!isBackfaceVisibilitySupported() && <>{children}</>}
{!isBackfaceVisibilitySupported() && (
<div data-testid="auth-card-active">
<FlipFaceContext.Provider value={isFlipped ? 'back' : 'front'}>
{children}
</FlipFaceContext.Provider>
</div>
)}
</>
);
};

View File

@@ -8,7 +8,7 @@ import {
} from '@/hooks/use-login-request';
import { useSystemConfig } from '@/hooks/use-system-request';
import { rsaPsw } from '@/utils';
import { useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
@@ -29,9 +29,214 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { BgSvg } from './bg';
import FlipCard3D from './card';
import FlipCard3D, { FlipFaceContext } from './card';
import './index.less';
type LoginFormContentProps = {
isLoginPage: boolean;
title: string;
form: ReturnType<typeof useForm>;
loading: boolean;
onCheck: (params: any) => Promise<void>;
changeTitle: () => void;
registerEnabled: boolean;
channels: { channel: string; icon?: string; display_name: string }[];
handleLoginWithChannel: (channel: string) => void;
t: ReturnType<typeof useTranslation>['t'];
};
function LoginFormContent({
isLoginPage,
title,
form,
loading,
onCheck,
changeTitle,
registerEnabled,
channels,
handleLoginWithChannel,
t,
}: LoginFormContentProps) {
const face = useContext(FlipFaceContext);
const isActiveFace = isLoginPage ? face === 'front' : face === 'back';
return (
<div className="flex flex-col items-center justify-center w-full">
<div className="text-center mb-8">
<h2 className="text-xl font-semibold text-text-primary">
{title === 'login' ? t('loginTitle') : t('signUpTitle')}
</h2>
</div>
<div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button ">
<Form {...form}>
<form
className="flex flex-col gap-8 text-text-primary "
data-testid="auth-form"
data-active={isActiveFace ? 'true' : undefined}
onSubmit={form.handleSubmit(onCheck)}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input
data-testid="auth-email"
placeholder={t('emailPlaceholder')}
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'register' && (
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('nicknameLabel')}</FormLabel>
<FormControl>
<Input
data-testid="auth-nickname"
placeholder={t('nicknamePlaceholder')}
autoComplete="username"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<div className="relative">
<Input
data-testid="auth-password"
type={'password'}
placeholder={t('passwordPlaceholder')}
autoComplete={
title === 'login'
? 'current-password'
: 'new-password'
}
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'login' && (
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex gap-2">
<Checkbox
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
/>
<FormLabel
className={cn(' hover:text-text-primary', {
'text-text-disabled': !field.value,
'text-text-primary': field.value,
})}
>
{t('rememberMe')}
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<ButtonLoading
data-testid="auth-submit"
type="submit"
loading={loading}
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8"
>
{title === 'login' ? t('login') : t('continue')}
</ButtonLoading>
{title === 'login' && channels && channels.length > 0 && (
<div className="mt-3 border">
{channels.map((item) => (
<Button
variant={'transparent'}
key={item.channel}
onClick={() => handleLoginWithChannel(item.channel)}
style={{ marginTop: 10 }}
>
<div className="flex items-center">
<SvgIcon
name={item.icon || 'sso'}
width={20}
height={20}
style={{ marginRight: 5 }}
/>
Sign in with {item.display_name}
</div>
</Button>
))}
</div>
)}
</form>
</Form>
{title === 'login' && registerEnabled && (
<div className="mt-10 text-right">
<p className="text-text-disabled text-sm">
{t('signInTip')}
<Button
data-testid="auth-toggle-register"
variant={'transparent'}
onClick={changeTitle}
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
>
{t('signUp')}
</Button>
</p>
</div>
)}
{title === 'register' && (
<div className="mt-10 text-right">
<p className="text-text-disabled text-sm">
{t('signUpTip')}
<Button
data-testid="auth-toggle-login"
variant={'transparent'}
onClick={changeTitle}
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
>
{t('login')}
</Button>
</p>
</div>
)}
</div>
</div>
);
}
const Login = () => {
const [title, setTitle] = useState('login');
const navigate = useNavigate();
@@ -174,182 +379,18 @@ const Login = () => {
{/* Login Form */}
<FlipCard3D isLoginPage={isLoginPage}>
<div className="flex flex-col items-center justify-center w-full">
<div className="text-center mb-8">
<h2 className="text-xl font-semibold text-text-primary">
{title === 'login' ? t('loginTitle') : t('signUpTitle')}
</h2>
</div>
<div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button ">
<Form {...form}>
<form
className="flex flex-col gap-8 text-text-primary "
onSubmit={form.handleSubmit(onCheck)}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input
placeholder={t('emailPlaceholder')}
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'register' && (
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('nicknameLabel')}</FormLabel>
<FormControl>
<Input
placeholder={t('nicknamePlaceholder')}
autoComplete="username"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<div className="relative">
<Input
type={'password'}
placeholder={t('passwordPlaceholder')}
autoComplete={
title === 'login'
? 'current-password'
: 'new-password'
}
{...field}
/>
{/* <button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-gray-500" />
) : (
<Eye className="h-4 w-4 text-gray-500" />
)}
</button> */}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'login' && (
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex gap-2">
<Checkbox
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
/>
<FormLabel
className={cn(' hover:text-text-primary', {
'text-text-disabled': !field.value,
'text-text-primary': field.value,
})}
>
{t('rememberMe')}
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<ButtonLoading
type="submit"
loading={loading}
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8"
>
{title === 'login' ? t('login') : t('continue')}
</ButtonLoading>
{title === 'login' && channels && channels.length > 0 && (
<div className="mt-3 border">
{channels.map((item) => (
<Button
variant={'transparent'}
key={item.channel}
onClick={() => handleLoginWithChannel(item.channel)}
style={{ marginTop: 10 }}
>
<div className="flex items-center">
<SvgIcon
name={item.icon || 'sso'}
width={20}
height={20}
style={{ marginRight: 5 }}
/>
Sign in with {item.display_name}
</div>
</Button>
))}
</div>
)}
</form>
</Form>
{title === 'login' && registerEnabled && (
<div className="mt-10 text-right">
<p className="text-text-disabled text-sm">
{t('signInTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
>
{t('signUp')}
</Button>
</p>
</div>
)}
{title === 'register' && (
<div className="mt-10 text-right">
<p className="text-text-disabled text-sm">
{t('signUpTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-accent-primary/90 hover:text-accent-primary hover:bg-transparent font-medium border-none transition-colors duration-200"
>
{t('login')}
</Button>
</p>
</div>
)}
</div>
</div>
<LoginFormContent
isLoginPage={isLoginPage}
title={title}
form={form}
loading={loading}
onCheck={onCheck}
changeTitle={changeTitle}
registerEnabled={registerEnabled}
channels={channels || []}
handleLoginWithChannel={handleLoginWithChannel}
t={t}
/>
</FlipCard3D>
</div>
</div>

View File

@@ -9,7 +9,7 @@ export function SavingButton({ loading }: SaveButtonProps) {
const { t } = useTranslation();
return (
<ButtonLoading type="submit" loading={loading}>
<ButtonLoading data-testid="chat-settings-save" type="submit" loading={loading}>
{t('common.save')}
</ButtonLoading>
);

View File

@@ -119,7 +119,7 @@ export default function Chat() {
}
return (
<section className="h-full flex flex-col">
<section className="h-full flex flex-col" data-testid="chat-detail">
<PageHeader>
<Breadcrumb>
<BreadcrumbList>

View File

@@ -49,7 +49,7 @@ export default function ChatList() {
}, [isCreate, handleShowCreateModal, searchParams, setSearchParams]);
return (
<section className="flex flex-col w-full flex-1">
<section className="flex flex-col w-full flex-1" data-testid="chats-list">
{data.dialogs?.length <= 0 && !searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
@@ -59,6 +59,7 @@ export default function ChatList() {
isSearch={!!searchString}
type={EmptyCardType.Chat}
onClick={() => handleShowCreateModal()}
testId="chats-empty-create"
/>
</div>
)}
@@ -71,7 +72,7 @@ export default function ChatList() {
onSearchChange={handleInputChange}
searchString={searchString}
>
<Button onClick={handleShowCreateModal}>
<Button onClick={handleShowCreateModal} data-testid="create-chat">
<Plus className="h-4 w-4" />
{t('chat.createChat')}
</Button>
@@ -86,6 +87,7 @@ export default function ChatList() {
isSearch={!!searchString}
type={EmptyCardType.Chat}
onClick={() => handleShowCreateModal()}
testId="chats-empty-create"
/>
</div>
)}

View File

@@ -57,7 +57,7 @@ export default function SearchPage() {
}, [isSearching]);
return (
<section>
<section data-testid="search-detail">
<PageHeader>
<Breadcrumb>
<BreadcrumbList>

View File

@@ -391,6 +391,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
</FormLabel>
<FormControl className="bg-bg-input">
<MultiSelect
data-testid="search-datasets-combobox"
options={datasetList}
onValueChange={(value) => {
handleDatasetSelectChange(value, field.onChange);
@@ -399,6 +400,8 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
placeholder={t('chat.knowledgeBasesMessage')}
maxCount={10}
defaultValue={field.value}
popoverTestId="datasets-options"
optionTestIdPrefix="datasets"
{...field}
/>
</FormControl>
@@ -583,7 +586,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
>
{t('search.cancelText')}
</Button>
<Button type="submit" disabled={formSubmitLoading}>
<Button data-testid="search-settings-save" type="submit" disabled={formSubmitLoading}>
{formSubmitLoading && (
<div className="size-4">
<Spin size="small" />

View File

@@ -65,7 +65,7 @@ export default function SearchList() {
}, [isCreate, openCreateModalFun, searchUrl, setSearchUrl]);
return (
<section className="w-full h-full flex flex-col">
<section className="w-full h-full flex flex-col" data-testid="search-list">
{(!list?.data?.search_apps?.length ||
list?.data?.search_apps?.length <= 0) &&
!searchString && (
@@ -77,6 +77,7 @@ export default function SearchList() {
type={EmptyCardType.Search}
isSearch={!!searchString}
onClick={() => openCreateModalFun()}
testId="search-empty-create"
/>
</div>
)}
@@ -92,6 +93,7 @@ export default function SearchList() {
>
<Button
variant={'default'}
data-testid="create-search"
onClick={() => {
openCreateModalFun();
}}
@@ -112,6 +114,7 @@ export default function SearchList() {
type={EmptyCardType.Search}
isSearch={!!searchString}
onClick={() => openCreateModalFun()}
testId="search-empty-create"
/>
</div>
)}

View File

@@ -75,7 +75,7 @@ export const ModelProviderCard: FC<IModelCardProps> = ({
};
return (
<div className={`w-full rounded-lg border border-border-button`}>
<div className={`w-full rounded-lg border border-border-button`} data-testid="added-model-card" data-provider={item.name}>
{/* Header */}
<div className="flex h-16 items-center justify-between p-4 cursor-pointer transition-colors text-text-secondary">
<div className="flex items-center space-x-3">

View File

@@ -73,6 +73,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
value: formData.llm_id,
options: modelOptions as SelectWithSearchFlagOptionType[],
tooltip: t('chatModelTip'),
testId: 'default-llm-combobox',
},
{
id: 'embd_id',
@@ -82,6 +83,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
LlmModelType.Embedding
] as SelectWithSearchFlagOptionType[],
tooltip: t('embeddingModelTip'),
testId: 'default-embedding-combobox',
},
{
id: 'img2txt_id',
@@ -129,6 +131,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
tooltip,
id,
isRequired,
testId,
}: {
id: string;
label: string;
@@ -136,6 +139,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
options: SelectWithSearchFlagOptionType[];
tooltip?: string;
isRequired?: boolean;
testId?: string;
}) => {
return (
<div className="flex gap-3">
@@ -162,6 +166,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
onChange={(value) => handleFieldChange(id, value)}
placeholder={t('selectModelPlaceholder')}
emptyData={t('modelEmptyTip')}
testId={testId}
/>
</div>
);

View File

@@ -79,7 +79,7 @@ export const AvailableModels: FC<{
};
return (
<div className=" text-text-primary h-full p-4">
<div className=" text-text-primary h-full p-4" data-testid="available-models-section">
<div className="text-text-primary text-base mb-4">
{t('availableModels')}
</div>
@@ -87,6 +87,7 @@ export const AvailableModels: FC<{
<div className="mb-6">
{/* <div className="relative"> */}
<SearchInput
data-testid="model-providers-search"
type="text"
placeholder={t('search')}
value={searchTerm}
@@ -131,6 +132,8 @@ export const AvailableModels: FC<{
{filteredModels.map((model) => (
<div
key={model.name}
data-testid="available-model-card"
data-provider={model.name}
className=" border border-border-button rounded-lg p-3 hover:bg-bg-input transition-colors group"
onClick={() => handleAddModel(model.name)}
>

View File

@@ -11,7 +11,7 @@ export const UsedModel = ({
}) => {
const { myLlmList: llmList } = useSelectLlmList();
return (
<div className="flex flex-col w-full gap-5 mb-4">
<div className="flex flex-col w-full gap-5 mb-4" data-testid="added-models-section">
<div className="text-text-primary text-2xl font-medium mb-2 mt-4">
{t('setting.addedModels')}
</div>

View File

@@ -86,6 +86,8 @@ const ApiKeyModal = ({
okText={t('save')}
cancelText={t('cancel')}
className="!w-[600px]"
testId="apikey-modal"
okButtonTestId="apikey-save"
>
<Form {...form}>
<div className="space-y-4 py-4">
@@ -103,6 +105,7 @@ const ApiKeyModal = ({
<FormControl>
<Input
{...field}
data-testid="apikey-input"
onKeyDown={handleKeyDown}
className="w-full"
/>

View File

@@ -68,6 +68,7 @@ export function SideBar() {
'bg-bg-card text-text-primary': active === item.key,
'bg-bg-base text-text-secondary': active !== item.key,
})}
data-testid={item.key === Routes.Model ? 'settings-nav-model-providers' : undefined}
onClick={handleMenuClick(item.key)}
>
<section className="flex items-center gap-2.5">