fix and add som usecases
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,8 +14,7 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
|
||||
BIN
PixPin_2025-09-15_15-18-02.png
Normal file
BIN
PixPin_2025-09-15_15-18-02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1 @@
|
||||
base_url: http://localhost:5173
|
||||
|
||||
20
conftest.py
Normal file
20
conftest.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def env_config():
|
||||
# A fixture to read the config file once per session.
|
||||
# In a real-world scenario, you might add logic here to select
|
||||
# different config files (e.g., dev, prod) based on a command-line argument.
|
||||
with open('config/dev_env.yaml', 'r') as file:
|
||||
return yaml.safe_load(file)
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def base_url(env_config):
|
||||
"""
|
||||
Provides the base URL for the application under test.
|
||||
"""
|
||||
url = env_config.get('base_url')
|
||||
if not url:
|
||||
raise Exception("'base_url' not found in config file.")
|
||||
return url
|
||||
@@ -0,0 +1,4 @@
|
||||
test_case,email,password,name,description
|
||||
valid_credentials,poop@shenjianl.cn,shenjianZ,Test User,Valid credentials for successful login
|
||||
invalid_password,test.user@example.com,WrongPassword,N/A,Correct email but incorrect password
|
||||
invalid_email,nouser@example.com,Password123,N/A,Non-existent email
|
||||
|
@@ -8,13 +8,16 @@ import {
|
||||
import { FormElementsPage } from "./components/pages/form-elements";
|
||||
import { DynamicContentPage } from "./components/pages/dynamic-content";
|
||||
import { cn } from "./lib/utils";
|
||||
import { LoginPage } from "./components/pages/login-page";
|
||||
import { HomePage } from "./components/pages/home-page";
|
||||
import { ProtectedRoute } from "./components/protected-route";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
<div className="flex h-screen bg-background text-foreground">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-white border-r">
|
||||
<aside className="w-64 bg-card border-r border-border">
|
||||
<div className="p-4">
|
||||
<Link to="/">
|
||||
<h1 className="text-2xl font-bold">Test App</h1>
|
||||
@@ -22,13 +25,26 @@ function App() {
|
||||
</div>
|
||||
<nav className="mt-4">
|
||||
<ul>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/home"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"block px-4 py-2 hover:bg-accent",
|
||||
isActive && "bg-muted"
|
||||
)
|
||||
}
|
||||
>
|
||||
Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/form-elements"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"block px-4 py-2 text-gray-700 hover:bg-gray-200",
|
||||
isActive && "bg-gray-300"
|
||||
"block px-4 py-2 hover:bg-accent",
|
||||
isActive && "bg-muted"
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -40,14 +56,27 @@ function App() {
|
||||
to="/dynamic-content"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"block px-4 py-2 text-gray-700 hover:bg-gray-200",
|
||||
isActive && "bg-gray-300"
|
||||
"block px-4 py-2 hover:bg-accent",
|
||||
isActive && "bg-muted"
|
||||
)
|
||||
}
|
||||
>
|
||||
Dynamic Content
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/login"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"block px-4 py-2 hover:bg-accent",
|
||||
isActive && "bg-muted"
|
||||
)
|
||||
}
|
||||
>
|
||||
Login Page
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -55,6 +84,15 @@ function App() {
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/home"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<HomePage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/form-elements" element={<FormElementsPage />} />
|
||||
<Route
|
||||
path="/dynamic-content"
|
||||
|
||||
77
frontend/src/components/pages/danger-zone.tsx
Normal file
77
frontend/src/components/pages/danger-zone.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export function DangerZone() {
|
||||
const { logout } = useAuth();
|
||||
const [confirmationText, setConfirmationText] = useState('');
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const requiredText = 'delete my account';
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirmationText === requiredText) {
|
||||
console.log('Account deletion confirmed.');
|
||||
// In a real app, you'd make an API call here.
|
||||
// For now, we just log out.
|
||||
setIsDialogOpen(false);
|
||||
logout();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border-red-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-red-600">Danger Zone</CardTitle>
|
||||
<CardDescription>These actions are permanent and cannot be undone.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-between items-center">
|
||||
<p className="font-medium">Delete this account</p>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">Delete Account</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
account and remove your data from our servers.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmation">
|
||||
Please type <span className="font-bold text-gray-800">{requiredText}</span> to confirm.
|
||||
</Label>
|
||||
<Input
|
||||
id="confirmation"
|
||||
value={confirmationText}
|
||||
onChange={(e) => setConfirmationText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={confirmationText !== requiredText}
|
||||
>
|
||||
I understand, delete my account
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
82
frontend/src/components/pages/home-page.tsx
Normal file
82
frontend/src/components/pages/home-page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ProfileEditor } from './profile-editor';
|
||||
import { SettingsPanel } from './settings-panel';
|
||||
import { DangerZone } from './danger-zone';
|
||||
import { Button } from '../ui/button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export function HomePage() {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">Welcome, {user?.name}!</h1>
|
||||
<Button onClick={handleLogout} variant="outline">Logout</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="dashboard" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="dashboard">Dashboard</TabsTrigger>
|
||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="dashboard" className="space-y-4">
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Test Cases</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">142</p>
|
||||
<p className="text-xs text-muted-foreground">+5 since last week</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="list-disc pl-5 text-sm">
|
||||
<li>Ran 'test_login_flow'</li>
|
||||
<li>Updated 'dynamic-content' page</li>
|
||||
<li>Added 3 new form elements</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Links</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col space-y-2">
|
||||
<a href="/form-elements" className="text-blue-600 hover:underline">Form Elements</a>
|
||||
<a href="/dynamic-content" className="text-blue-600 hover:underline">Dynamic Content</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="profile">
|
||||
<ProfileEditor />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings">
|
||||
<SettingsPanel />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<DangerZone />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
frontend/src/components/pages/login-page.tsx
Normal file
70
frontend/src/components/pages/login-page.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
|
||||
export function LoginPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const success = login(email, password);
|
||||
if (success) {
|
||||
navigate('/home');
|
||||
} else {
|
||||
setError('Invalid email or password');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit" className="w-full">
|
||||
Sign in
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
frontend/src/components/pages/profile-editor.tsx
Normal file
53
frontend/src/components/pages/profile-editor.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export function ProfileEditor() {
|
||||
const { user, updateUserInfo } = useAuth();
|
||||
const [name, setName] = useState(user?.name || '');
|
||||
const [feedback, setFeedback] = useState('');
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (name) {
|
||||
updateUserInfo(name);
|
||||
setFeedback('Profile updated successfully!');
|
||||
setTimeout(() => setFeedback(''), 3000); // Clear feedback after 3 seconds
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Personal Information</CardTitle>
|
||||
<CardDescription>Update your name here. Email is read-only.</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" value={user?.email || ''} disabled />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between items-center">
|
||||
<Button type="submit">Save Changes</Button>
|
||||
{feedback && <p className="text-sm text-green-600">{feedback}</p>}
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
51
frontend/src/components/pages/settings-panel.tsx
Normal file
51
frontend/src/components/pages/settings-panel.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
|
||||
export function SettingsPanel() {
|
||||
const { user, updateSettings } = useAuth();
|
||||
|
||||
if (!user) {
|
||||
return null; // Or a loading state
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Application Settings</CardTitle>
|
||||
<CardDescription>Manage your theme and notification preferences.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="notifications" className="font-medium">
|
||||
Enable Notifications
|
||||
</Label>
|
||||
<Switch
|
||||
id="notifications"
|
||||
checked={user.notifications}
|
||||
onCheckedChange={(checked) => updateSettings({ notifications: checked })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Label className="font-medium">Theme</Label>
|
||||
<RadioGroup
|
||||
value={user.theme}
|
||||
onValueChange={(value: 'light' | 'dark') => updateSettings({ theme: value })}
|
||||
className="flex space-x-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="light" id="light" />
|
||||
<Label htmlFor="light">Light</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="dark" id="dark" />
|
||||
<Label htmlFor="dark">Dark</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
14
frontend/src/components/protected-route.tsx
Normal file
14
frontend/src/components/protected-route.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
|
||||
export function ProtectedRoute({ children }: { children: JSX.Element }) {
|
||||
const { user } = useAuth();
|
||||
|
||||
if (!user) {
|
||||
// User is not authenticated
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
102
frontend/src/lib/auth.tsx
Normal file
102
frontend/src/lib/auth.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
|
||||
|
||||
// Define the shape of the user object
|
||||
interface User {
|
||||
email: string;
|
||||
name: string;
|
||||
theme: 'light' | 'dark';
|
||||
notifications: boolean;
|
||||
}
|
||||
|
||||
// Define the shape of the context
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
login: (email: string, pass:string) => boolean;
|
||||
logout: () => void;
|
||||
updateUserInfo: (name: string) => void;
|
||||
updateSettings: (settings: Partial<{ theme: 'light' | 'dark'; notifications: boolean }>) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
// Key for sessionStorage
|
||||
const USER_SESSION_KEY = 'test_app_user_session';
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(() => {
|
||||
// Initialize state from sessionStorage on component mount
|
||||
try {
|
||||
const storedUser = sessionStorage.getItem(USER_SESSION_KEY);
|
||||
return storedUser ? JSON.parse(storedUser) : null;
|
||||
} catch (error) {
|
||||
console.error("Failed to parse user from sessionStorage", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Effect to update sessionStorage and theme class whenever user state changes
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
try {
|
||||
if (user) {
|
||||
sessionStorage.setItem(USER_SESSION_KEY, JSON.stringify(user));
|
||||
// Apply theme
|
||||
root.classList.remove('light', 'dark');
|
||||
root.classList.add(user.theme);
|
||||
} else {
|
||||
sessionStorage.removeItem(USER_SESSION_KEY);
|
||||
root.classList.remove('dark'); // Default to light theme on logout
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save user to sessionStorage", error);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
|
||||
// Hardcoded credentials for simulation
|
||||
const login = (email: string, pass: string) => {
|
||||
if (email === 'poop@shenjianl.cn' && pass === 'shenjianZ') {
|
||||
// On login, set the full user object with defaults
|
||||
setUser({
|
||||
email,
|
||||
name: 'Test User',
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const updateUserInfo = (name: string) => {
|
||||
setUser(currentUser => {
|
||||
if (!currentUser) return null;
|
||||
return { ...currentUser, name };
|
||||
});
|
||||
};
|
||||
|
||||
const updateSettings = (settings: Partial<{ theme: 'light' | 'dark'; notifications: boolean }>) => {
|
||||
setUser(currentUser => {
|
||||
if (!currentUser) return null;
|
||||
return { ...currentUser, ...settings };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, login, logout, updateUserInfo, updateSettings }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
6
frontend/src/lib/utils.ts
Normal file
6
frontend/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { AuthProvider } from './lib/auth.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -11,24 +11,23 @@ class BasePage:
|
||||
improve test maintenance.
|
||||
"""
|
||||
|
||||
def __init__(self, driver: WebDriver, base_url: str = "http://120.53.89.168:90"):
|
||||
def __init__(self, driver: WebDriver):
|
||||
"""
|
||||
Initializes the BasePage with a WebDriver instance and a base URL.
|
||||
Initializes the BasePage with a WebDriver instance.
|
||||
|
||||
:param driver: The WebDriver instance to interact with the browser.
|
||||
:param base_url: The base URL of the web application under test.
|
||||
"""
|
||||
self.driver = driver
|
||||
self.base_url = base_url
|
||||
self.wait = WebDriverWait(driver, 10) # Default explicit wait of 10 seconds
|
||||
|
||||
def open_url(self, path: str):
|
||||
def open(self, base_url: str, path: str):
|
||||
"""
|
||||
Navigates to a specific path relative to the base URL.
|
||||
|
||||
:param base_url: The base URL of the web application.
|
||||
:param path: The relative path to open (e.g., "/form-elements").
|
||||
"""
|
||||
url = self.base_url + path
|
||||
url = base_url.rstrip('/') + path
|
||||
self.driver.get(url)
|
||||
|
||||
def find_element(self, locator: tuple) -> WebElement:
|
||||
@@ -177,3 +176,12 @@ class BasePage:
|
||||
:return: True if the text is present, False otherwise.
|
||||
"""
|
||||
return self.wait.until(EC.text_to_be_present_in_element(locator, text))
|
||||
|
||||
def wait_for_url_contains(self, url_substring: str):
|
||||
"""
|
||||
Waits until the current URL contains the given substring.
|
||||
|
||||
:param url_substring: The substring to look for in the URL.
|
||||
:return: True if the URL contains the substring, False otherwise.
|
||||
"""
|
||||
return self.wait.until(EC.url_contains(url_substring))
|
||||
|
||||
56
page_objects/danger_zone_page.py
Normal file
56
page_objects/danger_zone_page.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from typing import Self
|
||||
from selenium.webdriver.common import by
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from page_objects.base_page import BasePage
|
||||
|
||||
class DangerZonePage(BasePage):
|
||||
"""
|
||||
Page object for the Danger Zone page.
|
||||
"""
|
||||
|
||||
# Locators
|
||||
DELETE_ACCOUNT_BUTTON = (By.XPATH, '/html/body/div/div/main/div/div[3]/div/div[2]/button')
|
||||
CONFIRM_DELETE_BUTTON = (By.XPATH, '/html/body/div[3]/div[3]/button')
|
||||
CANCEL_DELETE_BUTTON = (By.XPATH, '/html/body/div[3]/button')
|
||||
DIALOG_TITLE = (By.XPATH, '/html/body/div[3]/div[1]/h2') # Assuming dialog title
|
||||
DIALOG_INPUT = (By.XPATH,'/html/body/div[3]/div[2]/input')
|
||||
_DANGER_ZONE_TITLE = (By.XPATH,'//*[@id="root"]/div/main/div/div[3]/div/div[1]/h3')
|
||||
|
||||
def get_page_header(self):
|
||||
"""
|
||||
Gets the header text of the page.
|
||||
Assumes the header is an h1 element.
|
||||
"""
|
||||
return self.get_text((By.TAG_NAME, "h1"))
|
||||
|
||||
def click_delete_account(self):
|
||||
"""
|
||||
Clicks the 'Delete Account' button to open the confirmation dialog.
|
||||
"""
|
||||
self.click(self.DELETE_ACCOUNT_BUTTON)
|
||||
|
||||
def verify_confirmation_dialog_exist(self):
|
||||
return self.get_text(self.DIALOG_TITLE)
|
||||
|
||||
def set_dialog_input(self):
|
||||
self.send_keys(self.DIALOG_INPUT,'delete my account')
|
||||
return ''
|
||||
def click_cancel_in_dialog(self, timeout=5):
|
||||
"""
|
||||
Safely clicks the cancel button in the confirmation dialog.
|
||||
"""
|
||||
self.click(self.CANCEL_DELETE_BUTTON)
|
||||
|
||||
|
||||
|
||||
|
||||
def click_confirm_in_dialog(self):
|
||||
"""
|
||||
Clicks the 'Confirm' button within the confirmation dialog.
|
||||
"""
|
||||
self.click(self.CONFIRM_DELETE_BUTTON)
|
||||
|
||||
def verify_danger_zone_exist(self):
|
||||
return self.get_text(self._DANGER_ZONE_TITLE)
|
||||
@@ -29,9 +29,9 @@ class DynamicContentPage(BasePage):
|
||||
|
||||
# --- Page Actions ---
|
||||
|
||||
def open(self):
|
||||
def open(self, base_url: str):
|
||||
"""Navigates to the dynamic content page."""
|
||||
self.open_url(self.page_path)
|
||||
super().open(base_url, self.page_path)
|
||||
|
||||
def get_delayed_text(self) -> str:
|
||||
"""
|
||||
|
||||
@@ -28,9 +28,9 @@ class FormElementsPage(BasePage):
|
||||
|
||||
# --- Page Actions ---
|
||||
|
||||
def open(self):
|
||||
def open(self, base_url: str):
|
||||
"""Navigates to the form elements page."""
|
||||
self.open_url(self.page_path)
|
||||
super().open(base_url, self.page_path)
|
||||
|
||||
def enter_text_in_input(self, text: str):
|
||||
"""Enters text into the main text input field."""
|
||||
|
||||
34
page_objects/home_dashboard_page.py
Normal file
34
page_objects/home_dashboard_page.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
from page_objects.base_page import BasePage
|
||||
|
||||
class HomeDashboardPage(BasePage):
|
||||
"""
|
||||
Page object for the main Home/Dashboard page, specifically handling the top navigation.
|
||||
"""
|
||||
|
||||
# Locators
|
||||
_PROFILE_TAB = (By.XPATH, "//button[@role='tab' and contains(@id,'trigger-profile')]")
|
||||
_SETTINGS_TAB = (By.XPATH, "//button[@role='tab' and contains(@id,'trigger-settings')]")
|
||||
DANGER_ZONE_LINK = (By.LINK_TEXT, "Danger Zone")
|
||||
LOGOUT_BUTTON = (By.XPATH, "//button[text()='Logout']")
|
||||
_DASHBORAD_TEST_CASES_TITLE = (By.XPATH,'//*[contains(@id,"content-dashboard")]/div/div[1]//h3')
|
||||
|
||||
def navigate_to_profile(self):
|
||||
"""
|
||||
Clicks the 'Profile' link to navigate to the Profile Editor page.
|
||||
"""
|
||||
self.click(self._PROFILE_TAB)
|
||||
|
||||
def navigate_to_settings(self):
|
||||
"""
|
||||
Clicks the 'Settings' link to navigate to the Settings Panel page.
|
||||
"""
|
||||
self.click(self._SETTINGS_TAB)
|
||||
|
||||
def verify_title_is_dashboarde(self):
|
||||
return self.get_text(self._DASHBORAD_TEST_CASES_TITLE)
|
||||
def logout(self):
|
||||
"""
|
||||
Clicks the 'Logout' button.
|
||||
"""
|
||||
self.click(self.LOGOUT_BUTTON)
|
||||
@@ -0,0 +1,49 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
from .base_page import BasePage
|
||||
|
||||
class HomePage(BasePage):
|
||||
"""
|
||||
Page Object for the Home page (Dashboard).
|
||||
"""
|
||||
|
||||
# --- Locators ---
|
||||
_WELCOME_MESSAGE = (By.XPATH, '//*[@id="root"]/div/main/div/div[1]/h1')
|
||||
_LOGOUT_BUTTON = (By.XPATH, "//button[text()='Logout']")
|
||||
_PROFILE_TAB = (By.XPATH, "//button[@role='tab' and text()='Profile']")
|
||||
_DASHBOARD_CONTENT = (By.XPATH, '/html/body/div/div/main/div/div[2]/div[2]/div')
|
||||
_DASHBORAD_TEST_CASES_TITLE = (By.XPATH,'//*[contains(@id,"content-dashboard")]/div/div[1]//h3')
|
||||
def __init__(self, driver):
|
||||
"""Initializes the HomePage with the WebDriver."""
|
||||
super().__init__(driver)
|
||||
self.page_path = "/" # The home page is at the root path after login
|
||||
|
||||
# --- Page Actions ---
|
||||
|
||||
def open(self, base_url: str):
|
||||
"""Navigates to the home page."""
|
||||
super().open(base_url, self.page_path)
|
||||
|
||||
def get_welcome_message(self) -> str:
|
||||
"""
|
||||
Gets the welcome message text from the page header.
|
||||
"""
|
||||
try:
|
||||
return self.find_visible_element(self._WELCOME_MESSAGE).text
|
||||
except:
|
||||
return ""
|
||||
|
||||
def is_dashboard_content_visible(self) -> bool:
|
||||
"""
|
||||
Checks if the main dashboard content area is visible.
|
||||
"""
|
||||
return self.get_text(self._DASHBORAD_TEST_CASES_TITLE) == 'Test Cases'
|
||||
|
||||
def switch_to_profile_tab(self):
|
||||
"""
|
||||
Clicks the 'Profile' tab to switch to the profile view.
|
||||
"""
|
||||
self.click(self._PROFILE_TAB)
|
||||
|
||||
def click_logout_button(self):
|
||||
"""Clicks the logout button."""
|
||||
self.click(self._LOGOUT_BUTTON)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
from .base_page import BasePage
|
||||
|
||||
class LoginPage(BasePage):
|
||||
"""
|
||||
Page Object for the Login page.
|
||||
"""
|
||||
|
||||
# --- Locators ---
|
||||
_EMAIL_INPUT = (By.ID, "email")
|
||||
_PASSWORD_INPUT = (By.ID, "password")
|
||||
_SIGN_IN_BUTTON = (By.XPATH, "//button[@type='submit']")
|
||||
_ERROR_MESSAGE = (By.XPATH, "//p[contains(@class, 'text-red-500')]")
|
||||
_LOGIN_FORM = (By.XPATH, "//form")
|
||||
|
||||
def __init__(self, driver):
|
||||
"""Initializes the LoginPage with the WebDriver."""
|
||||
super().__init__(driver)
|
||||
self.page_path = "/login"
|
||||
|
||||
# --- Page Actions ---
|
||||
|
||||
def open(self, base_url: str):
|
||||
"""Navigates to the login page."""
|
||||
super().open(base_url, self.page_path)
|
||||
|
||||
def enter_email(self, email: str):
|
||||
"""Enters the email into the email input field."""
|
||||
self.send_keys(self._EMAIL_INPUT, email)
|
||||
|
||||
def enter_password(self, password: str):
|
||||
"""Enters the password into the password input field."""
|
||||
self.send_keys(self._PASSWORD_INPUT, password)
|
||||
|
||||
def click_sign_in(self):
|
||||
"""Clicks the 'Sign in' button."""
|
||||
self.click(self._SIGN_IN_BUTTON)
|
||||
|
||||
def login(self, email: str, password: str):
|
||||
"""Performs a full login action."""
|
||||
self.enter_email(email)
|
||||
self.enter_password(password)
|
||||
self.click_sign_in()
|
||||
|
||||
def get_error_message(self) -> str:
|
||||
"""
|
||||
Waits for the error message to be visible and returns its text.
|
||||
Returns an empty string if the message is not found.
|
||||
"""
|
||||
try:
|
||||
return self.find_visible_element(self._ERROR_MESSAGE).text
|
||||
except:
|
||||
return ""
|
||||
|
||||
def is_login_form_visible(self) -> bool:
|
||||
"""Checks if the login form is visible on the page."""
|
||||
try:
|
||||
self.find_visible_element(self._LOGIN_FORM)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
53
page_objects/profile_editor_page.py
Normal file
53
page_objects/profile_editor_page.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from selenium.webdriver.common import by
|
||||
from selenium.webdriver.common.by import By
|
||||
from page_objects.base_page import BasePage
|
||||
|
||||
class ProfileEditorPage(BasePage):
|
||||
"""
|
||||
Page object for the Profile Editor page.
|
||||
"""
|
||||
|
||||
# Locators
|
||||
USERNAME_INPUT = (By.ID, "name")
|
||||
EMAIL_INPUT = (By.ID, "email")
|
||||
SAVE_BUTTON = (By.XPATH, "//button[text()='Save Changes']")
|
||||
SUCCESS_MESSAGE = (By.XPATH, "//*[contains(text(), 'Profile updated successfully')]") # Assuming a success message appears
|
||||
_PROFILE_TITLE = (By.XPATH,'//*[contains(@id,"content-profile")]//h3')
|
||||
|
||||
def get_page_header(self):
|
||||
"""
|
||||
Gets the header text of the page.
|
||||
Assumes the header is an h1 element.
|
||||
"""
|
||||
return self.get_text((By.TAG_NAME, "h1"))
|
||||
|
||||
def set_username(self, username: str):
|
||||
"""
|
||||
Enters the given username into the username input field.
|
||||
"""
|
||||
self.send_keys(self.USERNAME_INPUT, username)
|
||||
|
||||
def set_email(self, email: str):
|
||||
"""
|
||||
Enters the given email into the email input field.
|
||||
"""
|
||||
self.send_keys(self.EMAIL_INPUT, email)
|
||||
|
||||
def click_save_changes(self):
|
||||
"""
|
||||
Clicks the 'Save Changes' button.
|
||||
"""
|
||||
self.click(self.SAVE_BUTTON)
|
||||
|
||||
def is_success_message_displayed(self) -> bool:
|
||||
"""
|
||||
Checks if the success message is visible.
|
||||
"""
|
||||
try:
|
||||
return self.find_visible_element(self.SUCCESS_MESSAGE).is_displayed()
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def verify_title_is_profile(self):
|
||||
return self.get_text(self._PROFILE_TITLE)
|
||||
48
page_objects/settings_panel_page.py
Normal file
48
page_objects/settings_panel_page.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
from page_objects.base_page import BasePage
|
||||
|
||||
class SettingsPanelPage(BasePage):
|
||||
"""
|
||||
Page object for the Settings Panel page.
|
||||
"""
|
||||
|
||||
|
||||
# Locators
|
||||
_SETTING_TAB_TITLE = (By.XPATH, '//*[contains(@id,"content-settings")]//h3')
|
||||
NOTIFICATIONS_SWITCH = (By.ID, "notifications")
|
||||
THEME_SELECTOR = (By.ID, "theme")
|
||||
|
||||
def get_page_header(self):
|
||||
"""
|
||||
Gets the header text of the page.
|
||||
Assumes the header is an h1 element.
|
||||
"""
|
||||
return self.get_text((By.TAG_NAME, "h1"))
|
||||
|
||||
def verify_title_is_settings(self):
|
||||
return self.get_text(self._SETTING_TAB_TITLE)
|
||||
def toggle_notifications(self):
|
||||
"""
|
||||
Clicks the notifications switch to toggle it.
|
||||
"""
|
||||
self.click(self.NOTIFICATIONS_SWITCH)
|
||||
|
||||
def is_notifications_enabled(self) -> bool:
|
||||
"""
|
||||
Checks if the notifications switch is in the 'on' or 'selected' state.
|
||||
Note: This might need adjustment based on the actual HTML implementation (e.g., aria-checked).
|
||||
"""
|
||||
return self.is_element_selected(self.NOTIFICATIONS_SWITCH)
|
||||
|
||||
def select_theme(self, theme_name: str):
|
||||
"""
|
||||
Selects a theme from the dropdown.
|
||||
|
||||
:param theme_name: The visible text of the theme to select (e.g., "Dark").
|
||||
"""
|
||||
# This is a placeholder. The actual implementation depends on the select/dropdown component.
|
||||
# If it's a standard <select>, this would involve the Select class from Selenium.
|
||||
# from selenium.webdriver.support.ui import Select
|
||||
# select = Select(self.find_element(self.THEME_SELECTOR))
|
||||
# select.select_by_visible_text(theme_name)
|
||||
pass
|
||||
@@ -6,4 +6,5 @@ pytest-xdist
|
||||
pytest-rerunfailures
|
||||
pytest-ordering
|
||||
pytest-html
|
||||
pytest-metadata
|
||||
pytest-metadata
|
||||
PyYAML
|
||||
|
||||
@@ -10,8 +10,17 @@ def driver():
|
||||
Initializes and returns a Chrome WebDriver instance for each test function.
|
||||
Automatically quits the driver after the test function completes.
|
||||
"""
|
||||
# Initialize the Chrome WebDriver
|
||||
driver = webdriver.Chrome()
|
||||
# Configure Chrome options to disable the password manager and related pop-ups
|
||||
chrome_options = webdriver.ChromeOptions()
|
||||
prefs = {
|
||||
"credentials_enable_service": False,
|
||||
"profile.password_manager_enabled": False
|
||||
}
|
||||
chrome_options.add_experimental_option("prefs", prefs)
|
||||
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
|
||||
# Initialize the Chrome WebDriver with the specified options
|
||||
driver = webdriver.Chrome(options=chrome_options)
|
||||
|
||||
# Maximize the browser window
|
||||
driver.maximize_window()
|
||||
|
||||
1
tests/test_dynamic_content/__init__.py
Normal file
1
tests/test_dynamic_content/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -9,12 +9,12 @@ class TestDynamicContent:
|
||||
and dynamic elements.
|
||||
"""
|
||||
|
||||
def test_delayed_text_appears(self, driver):
|
||||
def test_delayed_text_appears(self, driver, base_url):
|
||||
"""
|
||||
Tests that the delayed text appears after a few seconds.
|
||||
"""
|
||||
dynamic_page = DynamicContentPage(driver)
|
||||
dynamic_page.open()
|
||||
dynamic_page.open(base_url)
|
||||
|
||||
# The waiting logic is inside the get_delayed_text method
|
||||
text = dynamic_page.get_delayed_text()
|
||||
@@ -24,12 +24,12 @@ class TestDynamicContent:
|
||||
"The delayed text did not appear or has incorrect content."
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_button_enables_after_click(self, driver):
|
||||
def test_button_enables_after_click(self, driver, base_url):
|
||||
"""
|
||||
Tests that a disabled button becomes enabled after an action.
|
||||
"""
|
||||
dynamic_page = DynamicContentPage(driver)
|
||||
dynamic_page.open()
|
||||
dynamic_page.open(base_url)
|
||||
|
||||
# Verify the button is initially disabled
|
||||
assert not dynamic_page.is_initially_disabled_button_enabled(), \
|
||||
@@ -44,12 +44,12 @@ class TestDynamicContent:
|
||||
assert dynamic_page.is_initially_disabled_button_enabled(), \
|
||||
"Button should be enabled after clicking the 'Enable' button."
|
||||
|
||||
def test_tabs_content_switching(self, driver):
|
||||
def test_tabs_content_switching(self, driver, base_url):
|
||||
"""
|
||||
Tests that content switches correctly when different tabs are clicked.
|
||||
"""
|
||||
dynamic_page = DynamicContentPage(driver)
|
||||
dynamic_page.open()
|
||||
dynamic_page.open(base_url)
|
||||
|
||||
# Switch to Password tab and verify content
|
||||
dynamic_page.switch_to_tab('password')
|
||||
@@ -62,12 +62,12 @@ class TestDynamicContent:
|
||||
assert "Account tab" in content, "Account tab content is not visible after switching back."
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_alert_handling(self, driver):
|
||||
def test_alert_handling(self, driver, base_url):
|
||||
"""
|
||||
Tests the triggering and handling of a browser alert.
|
||||
"""
|
||||
dynamic_page = DynamicContentPage(driver)
|
||||
dynamic_page.open()
|
||||
dynamic_page.open(base_url)
|
||||
|
||||
dynamic_page.trigger_alert()
|
||||
|
||||
@@ -75,12 +75,12 @@ class TestDynamicContent:
|
||||
|
||||
assert alert_text == "This is a browser alert!", "The alert text is incorrect."
|
||||
|
||||
def test_modal_dialog(self, driver):
|
||||
def test_modal_dialog(self, driver, base_url):
|
||||
"""
|
||||
Tests the opening of a modal dialog and verifies its content.
|
||||
"""
|
||||
dynamic_page = DynamicContentPage(driver)
|
||||
dynamic_page.open()
|
||||
dynamic_page.open(base_url)
|
||||
|
||||
dynamic_page.open_modal()
|
||||
|
||||
1
tests/test_form_elements/__init__.py
Normal file
1
tests/test_form_elements/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -7,12 +7,12 @@ class TestFormElements:
|
||||
Test suite for the Form Elements page.
|
||||
"""
|
||||
|
||||
def test_text_input(self, driver):
|
||||
def test_text_input(self, driver, base_url):
|
||||
"""
|
||||
Tests text entry and retrieval from the text input field.
|
||||
"""
|
||||
form_page = FormElementsPage(driver)
|
||||
form_page.open()
|
||||
form_page.open(base_url)
|
||||
|
||||
test_text = "Hello, Selenium!"
|
||||
form_page.enter_text_in_input(test_text)
|
||||
@@ -21,12 +21,12 @@ class TestFormElements:
|
||||
assert retrieved_text == test_text, f"Expected '{test_text}', but got '{retrieved_text}'"
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_checkbox_selection(self, driver):
|
||||
def test_checkbox_selection(self, driver, base_url):
|
||||
"""
|
||||
Tests the selection and deselection of a checkbox.
|
||||
"""
|
||||
form_page = FormElementsPage(driver)
|
||||
form_page.open()
|
||||
form_page.open(base_url)
|
||||
|
||||
assert not form_page.is_checkbox_selected(), "Checkbox should be deselected initially"
|
||||
|
||||
@@ -36,12 +36,12 @@ class TestFormElements:
|
||||
form_page.select_checkbox()
|
||||
assert not form_page.is_checkbox_selected(), "Checkbox should be deselected after clicking again"
|
||||
|
||||
def test_radio_button_selection(self, driver):
|
||||
def test_radio_button_selection(self, driver, base_url):
|
||||
"""
|
||||
Tests that only one radio button can be selected at a time.
|
||||
"""
|
||||
form_page = FormElementsPage(driver)
|
||||
form_page.open()
|
||||
form_page.open(base_url)
|
||||
|
||||
form_page.choose_radio_option(2)
|
||||
assert form_page.is_radio_option_selected(2), "Radio option 2 should be selected"
|
||||
@@ -51,12 +51,12 @@ class TestFormElements:
|
||||
assert form_page.is_radio_option_selected(3), "Radio option 3 should be selected"
|
||||
assert not form_page.is_radio_option_selected(2), "Radio option 2 should not be selected"
|
||||
|
||||
def test_dropdown_selection(self, driver):
|
||||
def test_dropdown_selection(self, driver, base_url):
|
||||
"""
|
||||
Tests selecting an option from the custom dropdown.
|
||||
"""
|
||||
form_page = FormElementsPage(driver)
|
||||
form_page.open()
|
||||
form_page.open(base_url)
|
||||
|
||||
fruit_to_select = "Banana"
|
||||
form_page.select_fruit_by_visible_text(fruit_to_select)
|
||||
@@ -65,22 +65,22 @@ class TestFormElements:
|
||||
assert selected_fruit == fruit_to_select, \
|
||||
f"Expected '{fruit_to_select}' to be selected, but got '{selected_fruit}'"
|
||||
|
||||
def test_disabled_button_state(self, driver):
|
||||
def test_disabled_button_state(self, driver, base_url):
|
||||
"""
|
||||
Verifies that the 'Disabled' button is indeed disabled.
|
||||
"""
|
||||
form_page = FormElementsPage(driver)
|
||||
form_page.open()
|
||||
form_page.open(base_url)
|
||||
|
||||
assert not form_page.is_disabled_button_enabled(), "The disabled button should not be enabled"
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_form_submission(self, driver):
|
||||
def test_form_submission(self, driver, base_url):
|
||||
"""
|
||||
A simple test to fill a field and click the submit button.
|
||||
"""
|
||||
form_page = FormElementsPage(driver)
|
||||
form_page.open()
|
||||
form_page.open(base_url)
|
||||
|
||||
form_page.enter_text_in_input("Test submission")
|
||||
form_page.click_submit_button()
|
||||
@@ -93,10 +93,10 @@ class TestFormElements:
|
||||
|
||||
|
||||
|
||||
def test_filure_case(self,driver):
|
||||
"""
|
||||
"""
|
||||
form_page = FormElementsPage(driver)
|
||||
form_page.open()
|
||||
print("case1")
|
||||
assert "" == "a", "error"
|
||||
# def test_filure_case(self,driver, base_url):
|
||||
# """
|
||||
# """
|
||||
# form_page = FormElementsPage(driver)
|
||||
# form_page.open(base_url)
|
||||
# print("case1")
|
||||
# assert "" == "a", "error"
|
||||
34
tests/test_home/conftest.py
Normal file
34
tests/test_home/conftest.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from page_objects.login_page import LoginPage
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def logged_in_driver(driver, base_url):
|
||||
"""
|
||||
A fixture that provides a logged-in WebDriver instance.
|
||||
|
||||
It performs the following steps:
|
||||
1. Reads valid login credentials from 'data/login_data.csv'.
|
||||
2. Navigates to the login page.
|
||||
3. Performs the login action.
|
||||
4. Yields the driver to the test function.
|
||||
|
||||
The test function then runs on a page that requires authentication.
|
||||
"""
|
||||
# 1. 读取有效的登录凭据 (假设第一行为有效数据)
|
||||
df = pd.read_csv('data/login_data.csv')
|
||||
valid_user = df[df['test_case'] == 'valid_credentials'].iloc[0]
|
||||
email = valid_user['email']
|
||||
password = valid_user['password']
|
||||
|
||||
# 2. 初始化登录页面并打开
|
||||
login_page = LoginPage(driver)
|
||||
login_page.open(base_url)
|
||||
|
||||
# 3. 执行登录
|
||||
login_page.login(email, password)
|
||||
|
||||
# 4. 将已登录的 driver 提供给测试用例
|
||||
yield driver
|
||||
|
||||
# 测试结束后,driver fixture 会自动关闭浏览器
|
||||
102
tests/test_home/test_dashboard.py
Normal file
102
tests/test_home/test_dashboard.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import time
|
||||
import pytest
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from page_objects.home_dashboard_page import HomeDashboardPage
|
||||
from page_objects.profile_editor_page import ProfileEditorPage
|
||||
from page_objects.settings_panel_page import SettingsPanelPage
|
||||
from page_objects.danger_zone_page import DangerZonePage
|
||||
|
||||
@pytest.mark.regression
|
||||
@pytest.mark.usefixtures("logged_in_driver")
|
||||
class TestDashboard:
|
||||
"""
|
||||
Test suite for the Home page and its nested pages (Dashboard).
|
||||
Requires user to be logged in.
|
||||
"""
|
||||
|
||||
def test_navigation_between_pages(self, logged_in_driver):
|
||||
"""
|
||||
Tests that the user can successfully navigate between the Profile,
|
||||
Settings, and Danger Zone pages.
|
||||
"""
|
||||
# Arrange
|
||||
dashboard_page = HomeDashboardPage(logged_in_driver)
|
||||
settings_page = SettingsPanelPage(logged_in_driver)
|
||||
danger_zone_page = DangerZonePage(logged_in_driver)
|
||||
profile_page = ProfileEditorPage(logged_in_driver)
|
||||
|
||||
# Act & Assert: Navigate back to dashborade and verify
|
||||
text = dashboard_page.verify_title_is_dashboarde()
|
||||
assert text == 'Test Cases' , 'error switch to dashhoard tab'
|
||||
|
||||
# Act & Assert: Navigate to Settings tab and verify
|
||||
dashboard_page.navigate_to_settings()
|
||||
text = settings_page.verify_title_is_settings()
|
||||
assert text == 'Application Settings' , 'error switch to settings tab'
|
||||
|
||||
# Act & Assert: Danger Zone and verify
|
||||
|
||||
text = danger_zone_page.verify_danger_zone_exist()
|
||||
assert text == 'Danger Zone' , 'no exists Danger Zone '
|
||||
|
||||
# Act & Assert: Navigate back to Profile and verify
|
||||
dashboard_page.navigate_to_profile()
|
||||
print(text)
|
||||
text = profile_page.verify_title_is_profile()
|
||||
assert text == 'Personal Information' , 'error switch to profile tab'
|
||||
|
||||
def test_profile_editor_interaction(self, logged_in_driver):
|
||||
"""
|
||||
Tests basic interaction with the profile editor form.
|
||||
"""
|
||||
# Arrange
|
||||
dashboard_page = HomeDashboardPage(logged_in_driver)
|
||||
profile_page = ProfileEditorPage(logged_in_driver)
|
||||
|
||||
# Act: Ensure we are on the profile page and update username
|
||||
dashboard_page.navigate_to_profile()
|
||||
new_username = "Automation Tester"
|
||||
profile_page.set_username(new_username)
|
||||
# self.profile_page.click_save_changes() # This action is commented out as it might not have a real backend effect
|
||||
|
||||
# Assert: Check if the value was updated in the input field
|
||||
updated_value = profile_page.find_element(profile_page.USERNAME_INPUT).get_attribute("value")
|
||||
assert updated_value == new_username
|
||||
|
||||
def test_danger_zone_dialog_flow(self, logged_in_driver):
|
||||
"""
|
||||
Tests that the 'Delete Account' button shows a confirmation dialog
|
||||
and that the cancel button closes it.
|
||||
"""
|
||||
# Arrange
|
||||
danger_zone_page = DangerZonePage(logged_in_driver)
|
||||
|
||||
# Act: Click the delete button
|
||||
danger_zone_page.click_delete_account()
|
||||
|
||||
# # Assert: The confirmation dialog is visible
|
||||
# assert danger_zone_page.verify_confirmation_dialog_exist(), \
|
||||
# "Confirmation dialog should be visible after clicking 'Delete Account'."
|
||||
|
||||
# Act: Click the cancel button
|
||||
time.sleep(1)
|
||||
danger_zone_page.click_cancel_in_dialog()
|
||||
text = danger_zone_page.verify_confirmation_dialog_exist()
|
||||
# Assert: The dialog is no longer visible
|
||||
assert text == 'Are you absolutely sure?', \
|
||||
"Confirmation dialog should not be visible after clicking 'Cancel'."
|
||||
|
||||
# Act: Click the delete button
|
||||
time.sleep(1)
|
||||
danger_zone_page.click_delete_account()
|
||||
danger_zone_page.set_dialog_input()
|
||||
danger_zone_page.click_confirm_in_dialog()
|
||||
# 等待跳转到登录页
|
||||
WebDriverWait(logged_in_driver, 5).until(
|
||||
lambda driver: '/login' in driver.current_url
|
||||
)
|
||||
|
||||
# 断言当前 URL
|
||||
current_url = logged_in_driver.current_url
|
||||
assert current_url.endswith('/login'), f"Expected to be on /login, but got {current_url}"
|
||||
|
||||
44
tests/test_home/test_home.py
Normal file
44
tests/test_home/test_home.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
from page_objects.home_page import HomePage
|
||||
from page_objects.login_page import LoginPage
|
||||
|
||||
@pytest.mark.usefixtures("logged_in_driver")
|
||||
class TestHomePage:
|
||||
"""
|
||||
Test suite for the Home page, which requires user authentication.
|
||||
"""
|
||||
|
||||
def test_welcome_message_is_displayed(self, logged_in_driver):
|
||||
"""
|
||||
Verifies that the correct welcome message is displayed after login.
|
||||
"""
|
||||
home_page = HomePage(logged_in_driver)
|
||||
welcome_text = home_page.get_welcome_message()
|
||||
# Assert against the hardcoded user name from auth.tsx
|
||||
assert "Welcome, Test User!" in welcome_text, "Welcome message is not correct or not found."
|
||||
|
||||
def test_dashboard_is_visible_on_load(self, logged_in_driver):
|
||||
"""
|
||||
Verifies that the dashboard tab content is visible by default.
|
||||
"""
|
||||
home_page = HomePage(logged_in_driver)
|
||||
assert home_page.is_dashboard_content_visible(), "Dashboard content should be visible by default."
|
||||
|
||||
def test_switch_to_profile_tab(self, logged_in_driver):
|
||||
"""
|
||||
Verifies that the user can switch to the Profile tab.
|
||||
"""
|
||||
home_page = HomePage(logged_in_driver)
|
||||
home_page.switch_to_profile_tab()
|
||||
# A proper test would assert that profile-specific elements are visible.
|
||||
|
||||
def test_logout_functionality(self, logged_in_driver):
|
||||
"""
|
||||
Verifies that clicking the logout button logs the user out and redirects to the login page.
|
||||
"""
|
||||
home_page = HomePage(logged_in_driver)
|
||||
home_page.click_logout_button()
|
||||
|
||||
# Verify redirection to the login page
|
||||
login_page = LoginPage(logged_in_driver)
|
||||
assert login_page.is_login_form_visible(), "After logout, the login form should be visible."
|
||||
61
tests/test_login/test_login.py
Normal file
61
tests/test_login/test_login.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import pytest
|
||||
from page_objects.login_page import LoginPage
|
||||
from page_objects.home_page import HomePage
|
||||
|
||||
@pytest.mark.regression
|
||||
class TestLogin:
|
||||
"""
|
||||
Test suite for the Login page, covering authentication scenarios.
|
||||
"""
|
||||
|
||||
def test_successful_login(self, driver, base_url):
|
||||
"""
|
||||
Tests that a user can successfully log in with valid credentials
|
||||
and is redirected to the home page.
|
||||
"""
|
||||
login_page = LoginPage(driver)
|
||||
home_page = HomePage(driver)
|
||||
|
||||
login_page.open(base_url)
|
||||
|
||||
# Use the hardcoded credentials from auth.tsx
|
||||
login_page.login("poop@shenjianl.cn", "shenjianZ")
|
||||
|
||||
# Explicitly wait for the URL to change to '/home'
|
||||
login_page.wait_for_url_contains("/home")
|
||||
|
||||
# Assert against the hardcoded user name
|
||||
welcome_message = home_page.get_welcome_message()
|
||||
assert "Welcome, Test User!" in welcome_message, \
|
||||
f"Welcome message was '{welcome_message}', but expected it to contain 'Welcome, Test User!'."
|
||||
|
||||
@pytest.mark.smoke
|
||||
@pytest.mark.parametrize("email, password, expected_error", [
|
||||
("user@example.com", "wrongpassword", "Invalid email or password"),
|
||||
("wronguser@example.com", "password123", "Invalid email or password"),
|
||||
])
|
||||
def test_login_with_invalid_credentials(self, driver, base_url, email, password, expected_error):
|
||||
"""
|
||||
Tests that login fails with various invalid credentials and the correct
|
||||
error message is displayed.
|
||||
"""
|
||||
login_page = LoginPage(driver)
|
||||
login_page.open(base_url)
|
||||
|
||||
login_page.login(email, password)
|
||||
|
||||
assert expected_error in login_page.get_error_message()
|
||||
assert "login" in driver.current_url
|
||||
|
||||
def test_accessing_protected_route_redirects_to_login(self, driver, base_url):
|
||||
"""
|
||||
Tests that attempting to access a protected page (e.g., /home) without
|
||||
being logged in redirects the user to the login page.
|
||||
"""
|
||||
login_page = LoginPage(driver)
|
||||
|
||||
home_url = base_url.rstrip('/') + "/home"
|
||||
driver.get(home_url)
|
||||
|
||||
assert "login" in driver.current_url
|
||||
assert login_page.is_login_form_visible(), "Login form should be visible after redirect"
|
||||
Reference in New Issue
Block a user