fix and add som usecases

This commit is contained in:
2025-09-15 17:41:07 +08:00
parent 838806b9e3
commit 414737fd48
37 changed files with 1133 additions and 51 deletions

3
.gitignore vendored
View File

@@ -14,8 +14,7 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1 @@
base_url: http://localhost:5173

20
conftest.py Normal file
View 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

View File

@@ -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
1 test_case email password name description
1 test_case email password name description
2 valid_credentials poop@shenjianl.cn shenjianZ Test User Valid credentials for successful login
3 invalid_password test.user@example.com WrongPassword N/A Correct email but incorrect password
4 invalid_email nouser@example.com Password123 N/A Non-existent email

View File

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

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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;
}

View 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))
}

View File

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

View File

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

View 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)

View File

@@ -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:
"""

View File

@@ -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."""

View 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)

View File

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

View File

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

View 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)

View 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

View File

@@ -6,4 +6,5 @@ pytest-xdist
pytest-rerunfailures
pytest-ordering
pytest-html
pytest-metadata
pytest-metadata
PyYAML

View File

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

View File

@@ -0,0 +1 @@

View File

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

View File

@@ -0,0 +1 @@

View File

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

View 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 会自动关闭浏览器

View 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}"

View 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."

View 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"