fix(m4): plans module audit fixes + E2E tests
This commit is contained in:
362
app/(app)/plans/page.tsx
Normal file
362
app/(app)/plans/page.tsx
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { RefreshCw, Package, Plus, Pencil, Trash2 } from "lucide-react";
|
||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/Card";
|
||||||
|
import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table";
|
||||||
|
import { Badge } from "@/components/ui/Badge";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { Input } from "@/components/ui/Input";
|
||||||
|
import { Modal } from "@/components/ui/Modal";
|
||||||
|
import { formatCurrency } from "@/lib/utils";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Plan {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: "PREPAID" | "POSTPAID";
|
||||||
|
speed: string;
|
||||||
|
price: string | number;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
interface PlansResponse { data: Plan[]; total: number; page: number; limit: number; }
|
||||||
|
|
||||||
|
const typeVariant: Record<string, "success" | "muted"> = {
|
||||||
|
PREPAID: "success",
|
||||||
|
POSTPAID: "muted",
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyForm = { name: "", type: "PREPAID" as "PREPAID" | "POSTPAID", speed: "", price: "", description: "" };
|
||||||
|
|
||||||
|
export default function PlansPage() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
|
const [editPlan, setEditPlan] = useState<Plan | null>(null);
|
||||||
|
const [deletePlan, setDeletePlan] = useState<Plan | null>(null);
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
const [createForm, setCreateForm] = useState({ ...emptyForm });
|
||||||
|
const [editForm, setEditForm] = useState({ ...emptyForm });
|
||||||
|
|
||||||
|
const { data, isLoading, isError, refetch } = useQuery<PlansResponse>({
|
||||||
|
queryKey: ["plans", search, page],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams({ page: String(page), limit: "20" });
|
||||||
|
if (search) params.set("search", search);
|
||||||
|
const res = await api.get<PlansResponse>(`/api/v1/plans?${params}`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await api.post("/api/v1/plans", {
|
||||||
|
name: createForm.name,
|
||||||
|
type: createForm.type,
|
||||||
|
speed: createForm.speed,
|
||||||
|
price: Number(createForm.price),
|
||||||
|
description: createForm.description || undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Plan created!");
|
||||||
|
setShowCreate(false);
|
||||||
|
setCreateForm({ ...emptyForm });
|
||||||
|
qc.invalidateQueries({ queryKey: ["plans"] });
|
||||||
|
},
|
||||||
|
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to create plan"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await api.patch(`/api/v1/plans/${editPlan!.id}`, {
|
||||||
|
name: editForm.name,
|
||||||
|
type: editForm.type,
|
||||||
|
speed: editForm.speed,
|
||||||
|
price: Number(editForm.price),
|
||||||
|
description: editForm.description || undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Plan updated!");
|
||||||
|
setEditPlan(null);
|
||||||
|
qc.invalidateQueries({ queryKey: ["plans"] });
|
||||||
|
},
|
||||||
|
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to update plan"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await api.delete(`/api/v1/plans/${deletePlan!.id}`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Plan deleted!");
|
||||||
|
setDeletePlan(null);
|
||||||
|
qc.invalidateQueries({ queryKey: ["plans"] });
|
||||||
|
},
|
||||||
|
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to delete plan"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const plans = data?.data ?? [];
|
||||||
|
const total = data?.total ?? 0;
|
||||||
|
|
||||||
|
const openEdit = (plan: Plan) => {
|
||||||
|
setEditForm({
|
||||||
|
name: plan.name,
|
||||||
|
type: plan.type,
|
||||||
|
speed: plan.speed,
|
||||||
|
price: String(plan.price),
|
||||||
|
description: plan.description ?? "",
|
||||||
|
});
|
||||||
|
setEditPlan(plan);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Plans</h1>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">{total} total plans</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={() => refetch()} variant="outline" size="sm" data-testid="btn-refresh">
|
||||||
|
<RefreshCw size={14} className="mr-1" />Refresh
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setShowCreate(true)} size="sm" data-testid="btn-add-plan">
|
||||||
|
<Plus size={14} className="mr-1" />Add Plan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<input
|
||||||
|
className="w-full max-w-sm border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Search plans..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => { setSearch(e.target.value); setPage(1); }}
|
||||||
|
data-testid="input-search"
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Type</Th>
|
||||||
|
<Th>Speed</Th>
|
||||||
|
<Th>Price</Th>
|
||||||
|
<Th>Description</Th>
|
||||||
|
<Th></Th>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
<Td colSpan={6}><div className="h-4 bg-gray-100 rounded animate-pulse" /></Td>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : isError ? (
|
||||||
|
<TableRow>
|
||||||
|
<Td colSpan={6}>
|
||||||
|
<p className="text-center py-6 text-red-400 text-sm">
|
||||||
|
Failed to load plans.{" "}
|
||||||
|
<button onClick={() => refetch()} className="underline">Retry</button>
|
||||||
|
</p>
|
||||||
|
</Td>
|
||||||
|
</TableRow>
|
||||||
|
) : plans.length === 0 ? (
|
||||||
|
<EmptyState colSpan={6} message="No plans found" icon={<Package size={24} />} />
|
||||||
|
) : (
|
||||||
|
plans.map(plan => (
|
||||||
|
<TableRow key={plan.id} data-testid="plan-row">
|
||||||
|
<Td className="font-medium">{plan.name}</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={typeVariant[plan.type] ?? "muted"}>{plan.type}</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td className="font-mono text-sm">{plan.speed}</Td>
|
||||||
|
<Td className="font-semibold">{formatCurrency(Number(plan.price))}</Td>
|
||||||
|
<Td className="text-gray-500 text-sm max-w-xs truncate">{plan.description ?? "—"}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => openEdit(plan)}
|
||||||
|
className="p-1.5 rounded hover:bg-blue-50 text-blue-600 transition-colors"
|
||||||
|
data-testid="btn-edit-plan"
|
||||||
|
title="Edit plan"
|
||||||
|
>
|
||||||
|
<Pencil size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeletePlan(plan)}
|
||||||
|
className="p-1.5 rounded hover:bg-red-50 text-red-500 transition-colors"
|
||||||
|
data-testid="btn-delete-plan"
|
||||||
|
title="Delete plan"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{total > 20 && (
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-t text-sm text-gray-500">
|
||||||
|
<span>Showing {(page - 1) * 20 + 1}–{Math.min(page * 20, total)} of {total}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button disabled={page === 1} onClick={() => setPage(p => p - 1)} className="px-3 py-1 border rounded-md disabled:opacity-40">Prev</button>
|
||||||
|
<button disabled={page * 20 >= total} onClick={() => setPage(p => p + 1)} className="px-3 py-1 border rounded-md disabled:opacity-40">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Create Plan Modal */}
|
||||||
|
<Modal isOpen={showCreate} onClose={() => { setShowCreate(false); setCreateForm({ ...emptyForm }); }} title="Add Plan" className="max-w-md" data-testid="modal-create-plan">
|
||||||
|
<div className="space-y-4" data-testid="modal-create-plan">
|
||||||
|
<Input
|
||||||
|
label="Plan Name"
|
||||||
|
value={createForm.name}
|
||||||
|
onChange={e => setCreateForm(f => ({ ...f, name: e.target.value }))}
|
||||||
|
placeholder="e.g. Basic 25Mbps"
|
||||||
|
data-testid="input-plan-name"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-sm font-medium text-gray-700">Type</label>
|
||||||
|
<select
|
||||||
|
className="border rounded-lg px-3 py-2 text-sm"
|
||||||
|
value={createForm.type}
|
||||||
|
onChange={e => setCreateForm(f => ({ ...f, type: e.target.value as "PREPAID" | "POSTPAID" }))}
|
||||||
|
data-testid="select-plan-type"
|
||||||
|
>
|
||||||
|
<option value="PREPAID">PREPAID</option>
|
||||||
|
<option value="POSTPAID">POSTPAID</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Speed"
|
||||||
|
value={createForm.speed}
|
||||||
|
onChange={e => setCreateForm(f => ({ ...f, speed: e.target.value }))}
|
||||||
|
placeholder="e.g. 25Mbps"
|
||||||
|
data-testid="input-plan-speed"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Price (₱)"
|
||||||
|
type="number"
|
||||||
|
value={createForm.price}
|
||||||
|
onChange={e => setCreateForm(f => ({ ...f, price: e.target.value }))}
|
||||||
|
placeholder="e.g. 999"
|
||||||
|
data-testid="input-plan-price"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Description (optional)"
|
||||||
|
value={createForm.description}
|
||||||
|
onChange={e => setCreateForm(f => ({ ...f, description: e.target.value }))}
|
||||||
|
placeholder="e.g. Perfect for households"
|
||||||
|
data-testid="input-plan-description"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2 justify-end pt-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => { setShowCreate(false); setCreateForm({ ...emptyForm }); }}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => createMutation.mutate()}
|
||||||
|
isLoading={createMutation.isPending}
|
||||||
|
disabled={!createForm.name || !createForm.speed || !createForm.price}
|
||||||
|
data-testid="btn-submit-create"
|
||||||
|
>
|
||||||
|
Create Plan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Edit Plan Modal */}
|
||||||
|
<Modal isOpen={!!editPlan} onClose={() => setEditPlan(null)} title={`Edit Plan: ${editPlan?.name ?? ""}`} className="max-w-md">
|
||||||
|
<div className="space-y-4" data-testid="modal-edit-plan">
|
||||||
|
<Input
|
||||||
|
label="Plan Name"
|
||||||
|
value={editForm.name}
|
||||||
|
onChange={e => setEditForm(f => ({ ...f, name: e.target.value }))}
|
||||||
|
data-testid="input-edit-name"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-sm font-medium text-gray-700">Type</label>
|
||||||
|
<select
|
||||||
|
className="border rounded-lg px-3 py-2 text-sm"
|
||||||
|
value={editForm.type}
|
||||||
|
onChange={e => setEditForm(f => ({ ...f, type: e.target.value as "PREPAID" | "POSTPAID" }))}
|
||||||
|
data-testid="select-edit-type"
|
||||||
|
>
|
||||||
|
<option value="PREPAID">PREPAID</option>
|
||||||
|
<option value="POSTPAID">POSTPAID</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Speed"
|
||||||
|
value={editForm.speed}
|
||||||
|
onChange={e => setEditForm(f => ({ ...f, speed: e.target.value }))}
|
||||||
|
data-testid="input-edit-speed"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Price (₱)"
|
||||||
|
type="number"
|
||||||
|
value={editForm.price}
|
||||||
|
onChange={e => setEditForm(f => ({ ...f, price: e.target.value }))}
|
||||||
|
data-testid="input-edit-price"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Description (optional)"
|
||||||
|
value={editForm.description}
|
||||||
|
onChange={e => setEditForm(f => ({ ...f, description: e.target.value }))}
|
||||||
|
data-testid="input-edit-description"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2 justify-end pt-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setEditPlan(null)}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => updateMutation.mutate()}
|
||||||
|
isLoading={updateMutation.isPending}
|
||||||
|
disabled={!editForm.name || !editForm.speed || !editForm.price}
|
||||||
|
data-testid="btn-submit-edit"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
<Modal isOpen={!!deletePlan} onClose={() => setDeletePlan(null)} title="Delete Plan" className="max-w-sm">
|
||||||
|
<div className="space-y-4" data-testid="modal-delete-plan">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Are you sure you want to delete <strong>{deletePlan?.name}</strong>? This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 justify-end pt-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setDeletePlan(null)} data-testid="btn-cancel-delete">Cancel</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deleteMutation.mutate()}
|
||||||
|
isLoading={deleteMutation.isPending}
|
||||||
|
data-testid="btn-confirm-delete"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard, roles: [] },
|
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard, roles: [] },
|
||||||
{ label: 'Clients', href: '/clients', icon: Users, roles: [] },
|
{ label: 'Clients', href: '/clients', icon: Users, roles: [] },
|
||||||
|
{ label: 'Plans', href: '/plans', icon: Wifi, roles: ['admin', 'staff'] },
|
||||||
{ label: 'Leads', href: '/leads', icon: UserPlus, roles: ['admin', 'staff'] },
|
{ label: 'Leads', href: '/leads', icon: UserPlus, roles: ['admin', 'staff'] },
|
||||||
{ label: 'Invoices', href: '/invoices', icon: FileText, roles: [] },
|
{ label: 'Invoices', href: '/invoices', icon: FileText, roles: [] },
|
||||||
{ label: 'Payments', href: '/payments', icon: CreditCard, roles: [] },
|
{ label: 'Payments', href: '/payments', icon: CreditCard, roles: [] },
|
||||||
|
|||||||
118
e2e/plans.spec.ts
Normal file
118
e2e/plans.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { login } from './helpers/auth';
|
||||||
|
|
||||||
|
const API = 'https://fiberops-api.juankibin.space/api/v1';
|
||||||
|
|
||||||
|
test.describe('Plans', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
await page.goto('/plans');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plans page renders with header and table', async ({ page }) => {
|
||||||
|
await expect(page.locator('h1:has-text("Plans")')).toBeVisible();
|
||||||
|
await expect(page.locator('table')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="btn-add-plan"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plans list loads rows from API', async ({ page }) => {
|
||||||
|
// Wait for either rows or empty state
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
const rows = page.locator('[data-testid="plan-row"]');
|
||||||
|
const empty = page.locator('text=No plans found');
|
||||||
|
const count = await rows.count();
|
||||||
|
const hasEmpty = await empty.isVisible();
|
||||||
|
// Either rows exist OR empty state is shown — both are valid
|
||||||
|
expect(count > 0 || hasEmpty).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add plan modal opens and submits', async ({ page }) => {
|
||||||
|
// Open create modal
|
||||||
|
await page.click('[data-testid="btn-add-plan"]');
|
||||||
|
|
||||||
|
// Modal should appear with form fields
|
||||||
|
await expect(page.locator('[data-testid="input-plan-name"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="select-plan-type"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="input-plan-speed"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="input-plan-price"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Fill form
|
||||||
|
const planName = `E2E Test Plan ${Date.now()}`;
|
||||||
|
await page.fill('[data-testid="input-plan-name"]', planName);
|
||||||
|
await page.selectOption('[data-testid="select-plan-type"]', 'PREPAID');
|
||||||
|
await page.fill('[data-testid="input-plan-speed"]', '25Mbps');
|
||||||
|
await page.fill('[data-testid="input-plan-price"]', '999');
|
||||||
|
await page.fill('[data-testid="input-plan-description"]', 'E2E test plan - safe to delete');
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
await page.click('[data-testid="btn-submit-create"]');
|
||||||
|
|
||||||
|
// Modal should close and success toast or new row appears
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Modal should be closed
|
||||||
|
const modalInput = page.locator('[data-testid="input-plan-name"]');
|
||||||
|
await expect(modalInput).not.toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edit plan modal opens and submits', async ({ page }) => {
|
||||||
|
// Wait for rows to load
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
const editBtns = page.locator('[data-testid="btn-edit-plan"]');
|
||||||
|
const count = await editBtns.count();
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
// No plans exist yet — create one first via API, then test edit
|
||||||
|
test.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click first edit button
|
||||||
|
await editBtns.first().click();
|
||||||
|
|
||||||
|
// Edit modal should open
|
||||||
|
await expect(page.locator('[data-testid="input-edit-name"]')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(page.locator('[data-testid="input-edit-speed"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="input-edit-price"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Change the speed
|
||||||
|
await page.fill('[data-testid="input-edit-speed"]', '50Mbps');
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
await page.click('[data-testid="btn-submit-edit"]');
|
||||||
|
|
||||||
|
// Modal should close
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
const editModal = page.locator('[data-testid="input-edit-name"]');
|
||||||
|
await expect(editModal).not.toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete plan shows confirmation dialog', async ({ page }) => {
|
||||||
|
// Wait for rows
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
const deleteBtns = page.locator('[data-testid="btn-delete-plan"]');
|
||||||
|
const count = await deleteBtns.count();
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
test.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click first delete button
|
||||||
|
await deleteBtns.first().click();
|
||||||
|
|
||||||
|
// Confirmation modal should appear
|
||||||
|
await expect(page.locator('[data-testid="modal-delete-plan"]')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(page.locator('[data-testid="btn-confirm-delete"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="btn-cancel-delete"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Cancel the delete (don't actually delete in tests)
|
||||||
|
await page.click('[data-testid="btn-cancel-delete"]');
|
||||||
|
|
||||||
|
// Modal should close
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const deleteModal = page.locator('[data-testid="modal-delete-plan"]');
|
||||||
|
await expect(deleteModal).not.toBeVisible({ timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user