feat: add Playwright E2E testing setup with POM and testids

- Install @playwright/test and Chromium browser
- Create playwright.config.ts with dev server integration
- Add data-testid attributes to LoginPage, DashboardPage, ServerCard, Sidebar
- Exclude tests-e2e from vitest config
- Create Page Object Models: LoginPage, DashboardPage
- Add 18 E2E tests: 6 login flow, 12 dashboard (happy, empty, error states)
- Add test:e2e and test:e2e:ui scripts to package.json
This commit is contained in:
Tran G. (Revernomad) Khoa
2026-04-17 00:01:11 +07:00
parent 88424675b5
commit 620429c9b8
12 changed files with 471 additions and 11 deletions

View File

@@ -11,7 +11,7 @@ export function Sidebar() {
const activeId = serverId ? parseInt(serverId) : null;
return (
<nav className="w-64 h-screen bg-surface-base border-r border-surface-raised flex flex-col">
<nav className="w-64 h-screen bg-surface-base border-r border-surface-raised flex flex-col" data-testid="sidebar">
<div className="px-6 py-5 border-b border-surface-raised">
<h1 className="text-text-primary font-bold text-lg tracking-tight">
<span className="text-accent">Languard</span>

View File

@@ -46,7 +46,7 @@ export function ServerCard({ server }: ServerCardProps) {
};
return (
<div className="neu-card p-5 flex flex-col gap-4">
<div className="neu-card p-5 flex flex-col gap-4" data-testid={`server-card-${server.id}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<StatusLed status={server.status} />

View File

@@ -12,7 +12,7 @@ export function DashboardPage() {
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="flex items-center justify-center h-64" data-testid="dashboard-loading">
<div className="text-text-secondary">Loading servers...</div>
</div>
);
@@ -20,14 +20,14 @@ export function DashboardPage() {
if (isError) {
return (
<div className="flex items-center justify-center h-64">
<div className="flex items-center justify-center h-64" data-testid="dashboard-error">
<div className="text-status-crashed">Failed to load servers</div>
</div>
);
}
return (
<div className="p-6">
<div className="p-6" data-testid="dashboard-content">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-text-primary font-bold text-xl">Dashboard</h1>
@@ -35,14 +35,14 @@ export function DashboardPage() {
{servers?.length ?? 0} server{servers?.length !== 1 ? "s" : ""} configured
</p>
</div>
<Link to="/servers/new" className="btn-primary flex items-center gap-1.5 text-sm">
<Link to="/servers/new" className="btn-primary flex items-center gap-1.5 text-sm" data-testid="add-server-btn">
<Plus size={14} />
Add Server
</Link>
</div>
{servers?.length === 0 ? (
<div className="neu-card p-12 text-center">
<div className="neu-card p-12 text-center" data-testid="dashboard-empty">
<p className="text-text-secondary">No servers configured yet.</p>
<Link to="/servers/new" className="btn-primary mt-4 inline-flex items-center gap-2">
<Plus size={14} />

View File

@@ -45,7 +45,7 @@ export function LoginPage() {
return (
<div className="min-h-screen bg-surface-base flex items-center justify-center p-4">
<div className="neu-card p-8 w-full max-w-sm">
<div className="neu-card p-8 w-full max-w-sm" data-testid="login-card">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-text-primary">
<span className="text-accent">Languard</span>
@@ -53,7 +53,7 @@ export function LoginPage() {
<p className="text-text-secondary text-sm mt-1">Server Manager</p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" data-testid="login-form">
<div>
<label className="block text-text-secondary text-sm mb-1.5" htmlFor="username">
Username
@@ -63,6 +63,7 @@ export function LoginPage() {
type="text"
autoComplete="username"
className="neu-input w-full"
data-testid="login-username"
{...register("username")}
/>
{errors.username && (
@@ -79,6 +80,7 @@ export function LoginPage() {
type="password"
autoComplete="current-password"
className="neu-input w-full"
data-testid="login-password"
{...register("password")}
/>
{errors.password && (
@@ -87,7 +89,7 @@ export function LoginPage() {
</div>
{error && (
<div className="bg-surface-recessed border border-status-crashed rounded-lg px-3 py-2">
<div className="bg-surface-recessed border border-status-crashed rounded-lg px-3 py-2" data-testid="login-error">
<p className="text-status-crashed text-sm">{error}</p>
</div>
)}
@@ -96,6 +98,7 @@ export function LoginPage() {
type="submit"
disabled={isSubmitting}
className="btn-primary w-full mt-2"
data-testid="login-submit"
>
{isSubmitting ? "Signing in..." : "Sign In"}
</button>