feat: Add comprehensive E2E tests with Playwright and enhance component test IDs
Add end-to-end testing infrastructure for the application: - Implemented Playwright E2E test suite with 31 passing tests across authentication and feature workflows - Created mock API fixtures for testing without requiring backend/database - Added data-testid attributes to major React components (Login, Register, TaskList, Events, SocialFeed, Profile, Navbar) - Set up test fixtures with test images (profile-pic.jpg, test-image.jpg) - Configured playwright.config.js for multi-browser testing (Chromium, Firefox, Safari) Test Coverage: - Authentication flows (register, login, logout, protected routes) - Task management (view, complete, filter, search) - Social feed (view posts, create post, like, view comments) - Events (view, join/RSVP, filter, view details) - User profile (view profile, streets, badges, statistics) - Premium features page - Leaderboard and rankings - Map view 🤖 Generated with OpenCode Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
This commit is contained in:
@@ -150,49 +150,49 @@ const Events = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="events-container">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>Events</h1>
|
||||
{connected && (
|
||||
<span className="badge badge-success">
|
||||
<span className="badge badge-success" data-testid="events-live-updates">
|
||||
<span className="mr-1">●</span> Live Updates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{events.length === 0 ? (
|
||||
<div className="alert alert-info">
|
||||
<div className="alert alert-info" data-testid="no-events-message">
|
||||
No events available at the moment. Check back later!
|
||||
</div>
|
||||
) : (
|
||||
<div className="row">
|
||||
<div className="row" data-testid="events-list">
|
||||
{events.map((event) => {
|
||||
const eventDate = new Date(event.date);
|
||||
const isUpcoming = eventDate > new Date();
|
||||
|
||||
return (
|
||||
<div key={event._id} className="col-md-6 mb-4">
|
||||
<div key={event._id} className="col-md-6 mb-4" data-testid={`event-card-${event._id}`}>
|
||||
<div className="card h-100">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{event.title}</h5>
|
||||
<p className="card-text">{event.description}</p>
|
||||
<h5 className="card-title" data-testid={`event-title-${event._id}`}>{event.title}</h5>
|
||||
<p className="card-text" data-testid={`event-description-${event._id}`}>{event.description}</p>
|
||||
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">
|
||||
<small className="text-muted" data-testid={`event-date-${event._id}`}>
|
||||
<strong>Date:</strong> {eventDate.toLocaleDateString()}{" "}
|
||||
{eventDate.toLocaleTimeString()}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">
|
||||
<small className="text-muted" data-testid={`event-location-${event._id}`}>
|
||||
<strong>Location:</strong> {event.location}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{event.organizer && (
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">
|
||||
<small className="text-muted" data-testid={`event-organizer-${event._id}`}>
|
||||
<strong>Organizer:</strong>{" "}
|
||||
{event.organizer.name || event.organizer}
|
||||
</small>
|
||||
@@ -204,10 +204,11 @@ const Events = () => {
|
||||
className={`badge badge-${
|
||||
isUpcoming ? "success" : "secondary"
|
||||
} mr-2`}
|
||||
data-testid={`event-status-${event._id}`}
|
||||
>
|
||||
{isUpcoming ? "Upcoming" : "Past"}
|
||||
</span>
|
||||
<span className="badge badge-info">
|
||||
<span className="badge badge-info" data-testid={`event-participants-${event._id}`}>
|
||||
{event.participants?.length || 0} Participants
|
||||
</span>
|
||||
</div>
|
||||
@@ -216,6 +217,7 @@ const Events = () => {
|
||||
<button
|
||||
className="btn btn-primary btn-sm mt-3 btn-block"
|
||||
onClick={() => rsvp(event._id)}
|
||||
data-testid={`rsvp-btn-${event._id}`}
|
||||
>
|
||||
RSVP
|
||||
</button>
|
||||
|
||||
@@ -44,11 +44,11 @@ const Login = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="container" data-testid="login-container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-6">
|
||||
<h1 className="text-center">Login</h1>
|
||||
<form onSubmit={onSubmit}>
|
||||
<form onSubmit={onSubmit} data-testid="login-form">
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="email"
|
||||
@@ -59,6 +59,7 @@ const Login = () => {
|
||||
placeholder="Email"
|
||||
required
|
||||
disabled={loading}
|
||||
data-testid="email-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
@@ -71,12 +72,14 @@ const Login = () => {
|
||||
placeholder="Password"
|
||||
required
|
||||
disabled={loading}
|
||||
data-testid="password-input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-block"
|
||||
disabled={loading}
|
||||
data-testid="login-submit-btn"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
|
||||
@@ -8,34 +8,34 @@ const Navbar = () => {
|
||||
const authLinks = (
|
||||
<ul className="navbar-nav ml-auto">
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/map">Map</Link>
|
||||
<Link className="nav-link" to="/map" data-testid="nav-map">Map</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/tasks">Tasks</Link>
|
||||
<Link className="nav-link" to="/tasks" data-testid="nav-tasks">Tasks</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/feed">Feed</Link>
|
||||
<Link className="nav-link" to="/feed" data-testid="nav-feed">Feed</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/events">Events</Link>
|
||||
<Link className="nav-link" to="/events" data-testid="nav-events">Events</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/rewards">Rewards</Link>
|
||||
<Link className="nav-link" to="/rewards" data-testid="nav-rewards">Rewards</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/leaderboard">Leaderboard</Link>
|
||||
<Link className="nav-link" to="/leaderboard" data-testid="nav-leaderboard">Leaderboard</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/analytics">Analytics</Link>
|
||||
<Link className="nav-link" to="/analytics" data-testid="nav-analytics">Analytics</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/profile">Profile</Link>
|
||||
<Link className="nav-link" to="/profile" data-testid="nav-profile">Profile</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/premium">Premium</Link>
|
||||
<Link className="nav-link" to="/premium" data-testid="nav-premium">Premium</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a onClick={logout} href="#!" className="nav-link">Logout</a>
|
||||
<a onClick={logout} href="#!" className="nav-link" data-testid="logout-button">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
@@ -43,18 +43,18 @@ const Navbar = () => {
|
||||
const guestLinks = (
|
||||
<ul className="navbar-nav ml-auto">
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/register">Register</Link>
|
||||
<Link className="nav-link" to="/register" data-testid="nav-register">Register</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/login">Login</Link>
|
||||
<Link className="nav-link" to="/login" data-testid="nav-login">Login</Link>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className="navbar navbar-expand-sm navbar-dark bg-dark mb-4">
|
||||
<nav className="navbar navbar-expand-sm navbar-dark bg-dark mb-4" data-testid="navbar">
|
||||
<div className="container">
|
||||
<Link className="navbar-brand" to="/">Adopt-a-Street</Link>
|
||||
<Link className="navbar-brand" to="/" data-testid="navbar-brand">Adopt-a-Street</Link>
|
||||
<div className="collapse navbar-collapse">
|
||||
{auth.isAuthenticated ? authLinks : guestLinks}
|
||||
</div>
|
||||
|
||||
@@ -89,40 +89,41 @@ const Profile = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="profile-container">
|
||||
<h1>{user.name}'s Profile</h1>
|
||||
|
||||
<div className="card mb-4">
|
||||
<div className="card mb-4" data-testid="profile-info-card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Profile Information</h5>
|
||||
<p className="card-text">
|
||||
<p className="card-text" data-testid="profile-email">
|
||||
<strong>Email:</strong> {user.email}
|
||||
</p>
|
||||
<p className="card-text">
|
||||
<p className="card-text" data-testid="profile-points">
|
||||
<strong>Points:</strong>{" "}
|
||||
<span className="badge badge-primary">{user.points || 0}</span>
|
||||
</p>
|
||||
{user.isPremium && (
|
||||
<p className="card-text">
|
||||
<p className="card-text" data-testid="premium-badge">
|
||||
<span className="badge badge-warning">Premium Member</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card mb-4">
|
||||
<div className="card mb-4" data-testid="adopted-streets-card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Adopted Streets</h5>
|
||||
{user.adoptedStreets && user.adoptedStreets.length > 0 ? (
|
||||
<ul className="list-group">
|
||||
<ul className="list-group" data-testid="streets-list">
|
||||
{user.adoptedStreets.map((street) => (
|
||||
<li key={street._id || street} className="list-group-item">
|
||||
<li key={street._id || street} className="list-group-item" data-testid={`street-item-${street._id || street}`}>
|
||||
{street.name || street}
|
||||
{street.status && (
|
||||
<span
|
||||
className={`badge badge-${
|
||||
street.status === "available" ? "success" : "primary"
|
||||
} ml-2`}
|
||||
data-testid={`street-status-${street._id || street}`}
|
||||
>
|
||||
{street.status}
|
||||
</span>
|
||||
@@ -131,7 +132,7 @@ const Profile = () => {
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-muted">
|
||||
<p className="text-muted" data-testid="no-streets-message">
|
||||
You haven't adopted any streets yet. Visit the map to adopt a
|
||||
street!
|
||||
</p>
|
||||
@@ -139,21 +140,21 @@ const Profile = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card mb-4">
|
||||
<div className="card mb-4" data-testid="badges-card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Badges</h5>
|
||||
{user.badges && user.badges.length > 0 ? (
|
||||
<ul className="list-group">
|
||||
<ul className="list-group" data-testid="badges-list">
|
||||
{user.badges.map((badge, index) => (
|
||||
<li key={index} className="list-group-item">
|
||||
<span className="badge badge-success mr-2">
|
||||
<li key={index} className="list-group-item" data-testid={`badge-item-${index}`}>
|
||||
<span className="badge badge-success mr-2" data-testid={`badge-${badge}`}>
|
||||
{badge}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-muted">
|
||||
<p className="text-muted" data-testid="no-badges-message">
|
||||
No badges earned yet. Complete tasks and participate in events to
|
||||
earn badges!
|
||||
</p>
|
||||
@@ -162,15 +163,15 @@ const Profile = () => {
|
||||
</div>
|
||||
|
||||
{user.tasksCompleted !== undefined && (
|
||||
<div className="card mb-4">
|
||||
<div className="card mb-4" data-testid="statistics-card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Statistics</h5>
|
||||
<p className="card-text">
|
||||
<p className="card-text" data-testid="tasks-completed">
|
||||
<strong>Tasks Completed:</strong>{" "}
|
||||
<span className="badge badge-info">{user.tasksCompleted}</span>
|
||||
</p>
|
||||
{user.eventsAttended !== undefined && (
|
||||
<p className="card-text">
|
||||
<p className="card-text" data-testid="events-attended">
|
||||
<strong>Events Attended:</strong>{" "}
|
||||
<span className="badge badge-info">{user.eventsAttended}</span>
|
||||
</p>
|
||||
|
||||
@@ -48,11 +48,11 @@ const Register = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="container" data-testid="register-container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-6">
|
||||
<h1 className="text-center">Register</h1>
|
||||
<form onSubmit={onSubmit}>
|
||||
<form onSubmit={onSubmit} data-testid="register-form">
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
@@ -63,6 +63,7 @@ const Register = () => {
|
||||
placeholder="Name"
|
||||
required
|
||||
disabled={loading}
|
||||
data-testid="name-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
@@ -75,6 +76,7 @@ const Register = () => {
|
||||
placeholder="Email"
|
||||
required
|
||||
disabled={loading}
|
||||
data-testid="email-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
@@ -87,12 +89,14 @@ const Register = () => {
|
||||
placeholder="Password"
|
||||
required
|
||||
disabled={loading}
|
||||
data-testid="password-input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-block"
|
||||
disabled={loading}
|
||||
data-testid="register-submit-btn"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
|
||||
@@ -205,21 +205,21 @@ const SocialFeed = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="social-feed-container">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>Social Feed</h1>
|
||||
{connected && (
|
||||
<span className="badge badge-success">
|
||||
<span className="badge badge-success" data-testid="feed-live-updates">
|
||||
<span className="mr-1">●</span> Live Updates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{auth.isAuthenticated && (
|
||||
<div className="card mb-4">
|
||||
<div className="card mb-4" data-testid="create-post-card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Create a Post</h5>
|
||||
<form onSubmit={onSubmit}>
|
||||
<form onSubmit={onSubmit} data-testid="create-post-form">
|
||||
<div className="form-group">
|
||||
<textarea
|
||||
className="form-control"
|
||||
@@ -229,12 +229,14 @@ const SocialFeed = () => {
|
||||
placeholder="What's on your mind?"
|
||||
required
|
||||
disabled={submitting}
|
||||
data-testid="post-content-textarea"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={submitting || !content.trim()}
|
||||
data-testid="create-post-btn"
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
@@ -255,16 +257,16 @@ const SocialFeed = () => {
|
||||
)}
|
||||
|
||||
{posts.length === 0 ? (
|
||||
<div className="alert alert-info">
|
||||
<div className="alert alert-info" data-testid="no-posts-message">
|
||||
No posts yet. Be the first to share something!
|
||||
</div>
|
||||
) : (
|
||||
<ul className="list-group">
|
||||
<ul className="list-group" data-testid="posts-list">
|
||||
{posts.map((post) => (
|
||||
<li key={post._id} className="list-group-item">
|
||||
<li key={post._id} className="list-group-item" data-testid={`post-item-${post._id}`}>
|
||||
<div className="mb-2">
|
||||
<p className="mb-2">{post.content}</p>
|
||||
<small className="text-muted">
|
||||
<p className="mb-2" data-testid={`post-content-${post._id}`}>{post.content}</p>
|
||||
<small className="text-muted" data-testid={`post-metadata-${post._id}`}>
|
||||
By: <strong>{post.user?.name || "Unknown User"}</strong>
|
||||
{post.createdAt && (
|
||||
<span className="ml-2">
|
||||
@@ -278,6 +280,7 @@ const SocialFeed = () => {
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => likePost(post._id)}
|
||||
disabled={!auth.isAuthenticated || likingPostId === post._id}
|
||||
data-testid={`like-btn-${post._id}`}
|
||||
>
|
||||
{likingPostId === post._id ? (
|
||||
<>
|
||||
@@ -295,7 +298,7 @@ const SocialFeed = () => {
|
||||
)}
|
||||
</button>
|
||||
{post.comments && post.comments.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="mt-2" data-testid={`post-comments-${post._id}`}>
|
||||
<small className="text-muted">
|
||||
{post.comments.length} comment{post.comments.length !== 1 ? "s" : ""}
|
||||
</small>
|
||||
|
||||
@@ -142,26 +142,27 @@ const TaskList = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="task-list-container">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>Task List</h1>
|
||||
{connected && (
|
||||
<span className="badge badge-success">
|
||||
<span className="badge badge-success" data-testid="live-updates-badge">
|
||||
<span className="mr-1">●</span> Live Updates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tasks.length === 0 ? (
|
||||
<div className="alert alert-info">
|
||||
<div className="alert alert-info" data-testid="no-tasks-message">
|
||||
No tasks available at the moment. Check back later!
|
||||
</div>
|
||||
) : (
|
||||
<ul className="list-group">
|
||||
<ul className="list-group" data-testid="tasks-list">
|
||||
{tasks.map((task) => (
|
||||
<li
|
||||
key={task._id}
|
||||
className="list-group-item d-flex justify-content-between align-items-center"
|
||||
data-testid={`task-item-${task._id}`}
|
||||
>
|
||||
<div>
|
||||
<strong>{task.description}</strong>
|
||||
@@ -170,6 +171,7 @@ const TaskList = () => {
|
||||
className={`badge badge-${
|
||||
task.status === "pending" ? "warning" : "success"
|
||||
} mr-2`}
|
||||
data-testid={`task-status-${task._id}`}
|
||||
>
|
||||
{task.status}
|
||||
</span>
|
||||
@@ -187,6 +189,7 @@ const TaskList = () => {
|
||||
className="btn btn-sm btn-success"
|
||||
onClick={() => completeTask(task._id)}
|
||||
disabled={completingTaskId === task._id}
|
||||
data-testid={`complete-task-btn-${task._id}`}
|
||||
>
|
||||
{completingTaskId === task._id ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user