easy-invoice-pdf/src/components/buyer-dialog.tsx
Vlad Sazonau e6f54d5dfc
feat: add e2e tests with playwright and other improvements (#79)
* feat: Add language attribute to date input fields in invoice form components

- Include `lang="en"` attribute in date input fields of `InvoiceForm` and `GeneralInformation` components for improved accessibility and localization support.

* fix: Update language attribute for date input fields in invoice form components

* refactor: Improve layout and organization of invoice components

- Remove unnecessary margin from the main container in the Home component.
- Wrap the share invoice button and PDF download link in a fragment for better structure.
- Adjust margins for the ProjectInfo and action button container for improved spacing.
- Update the InvoicePDFViewer height to use full height for better responsiveness.
- Remove the deprecated RegenerateInvoiceButton component to streamline the codebase.
- Update the InvoiceClientPage to accept handleShareInvoice prop for better functionality.
- Clean up unused language attributes in date input fields across invoice form components.

* feat: Integrate Playwright for end-to-end testing and enhance invoice form components

- Add Playwright configuration and dependencies for E2E testing.
- Create GitHub Actions workflow for automated E2E tests on deployment.
- Implement initial E2E tests for the Invoice Generator Page, verifying UI elements and form functionality.
- Refactor invoice form components to include data-testid attributes for better testability.
- Update .gitignore to exclude Playwright-related files and directories.

* chore: Update GitHub Actions workflow for E2E testing and enhance test coverage

- Upgrade pnpm version from 8 to 10 in the E2E workflow for improved package management.
- Add new test case to verify header buttons and links on the Invoice Generator Page, ensuring UI elements are displayed correctly and have the expected attributes.

* chore: Enhance ESLint configuration for Playwright integration

- Add Playwright ESLint plugin to package.json for improved E2E testing support.
- Update .eslintrc.json to include overrides for E2E test files.
- Clean up GitHub Actions workflow by removing unnecessary pnpm version specification.

* chore: Update Playwright configuration and improve test assertions

- Increase timeout for expect assertions and test execution from 15 seconds to 30 seconds for better stability in E2E tests.
- Comment out mobile viewport tests to streamline configuration and focus on desktop testing.

* chore: Update configuration and refactor invoice form components

- Add compiler options to remove console logs in production and enhance logging for fetch requests in next.config.mjs.
- Update package.json to include new type definitions for ua-parser-js and add ua-parser-js as a dependency.
- Refactor invoice form components to remove form prefix IDs, simplifying data-testid attributes for better testability.
- Introduce DeviceContext for managing device type state and improve responsiveness in invoice form components.
- Implement server-side device detection using user agent parsing for better rendering on mobile and desktop views.
- Update media query hooks to streamline device type checks across components.

* chore: Update Playwright configuration and enhance invoice form tests

- Reduce timeout for expect assertions from 30 seconds to 15 seconds for improved test performance.
- Add new test for handling currency switching in the Invoice Generator Page, verifying correct currency display and calculations.
- Refactor buyer and seller information components to include tooltip messages and improve accessibility with aria attributes.
- Update BuyerDialog and BuyerManagement components to enhance user experience with better visibility and edit functionality for buyer details.

* chore: Update Playwright installation command in GitHub Actions workflow

- Modify Playwright installation command to remove explicit browser specification, allowing for default browser installation with dependencies.

* chore: Update GitHub Actions E2E workflow for Playwright report handling

- Change condition for uploading Playwright report to ensure it uploads regardless of test outcome.
- Reduce retention days for uploaded reports from 5 to 3 for better resource management.

* chore: Update Playwright installation command in GitHub Actions workflow

- Specify installation of Chromium and WebKit browsers along with dependencies for enhanced testing capabilities.

* chore: Enhance E2E tests for seller and buyer management functionality

- Add tests to verify the deletion process for sellers and buyers, including confirmation dialogs and success messages.
- Ensure localStorage data is correctly saved and parsed for both seller and buyer information.
- Introduce default data constants for sellers and buyers to streamline test setup.
- Improve accessibility by adding screen reader text for delete buttons in the seller management component.

* chore: Pin versions of GitHub Actions in E2E workflow for stability

- Update actions/checkout, pnpm/action-setup, actions/setup-node, and actions/upload-artifact to specific versions for improved reliability and security.
- Comment added to clarify the rationale for using pinned versions.

* chore: Add E2E test for accordion items visibility and localStorage state management

- Implement test to verify that accordion items are visible, collapsible, and their state is correctly saved in localStorage.
- Ensure state persistence across page reloads and validate updated states after toggling sections.
- Introduce ACCORDION_STATE_LOCAL_STORAGE_KEY and AccordionState type for better type safety and clarity.

* chore: Update Playwright configuration and add comprehensive E2E tests for seller and buyer management

- Increase timeout for expect assertions and test execution from 30 seconds to 60 seconds for improved stability in E2E tests.
- Introduce new E2E tests for seller and buyer management, covering creation, editing, and deletion processes, including confirmation dialogs and success messages.
- Ensure localStorage data is correctly saved and parsed for both seller and buyer information.
- Implement detailed validation for form fields and visibility toggles in seller and buyer management dialogs.
- Enhance accessibility by adding screen reader text for buttons and tooltips in the management components.

* chore: Refactor Playwright configuration and enhance invoice item validation tests

- Introduce a constant for timeout values in Playwright configuration for consistency and maintainability.
- Add comprehensive validation tests for amount, net price, and VAT fields in the invoice items section, ensuring proper error messages for invalid inputs.
- Update expected error messages in the schema to match the new formatting for better clarity.
- Improve test structure by utilizing descriptive variable names and modularizing input handling for better readability.

* chore: add pdf e2e tests

- Add `pdf-parse` and its type definitions to package.json for PDF handling capabilities.
- Increase Playwright timeout from 30 seconds to 60 seconds for improved test stability.
- Introduce comprehensive E2E tests for PDF generation, verifying content in both English and Polish.
- Implement cleanup procedures for test downloads to ensure a clean testing environment.
- Validate invoice data updates in the generated PDF, ensuring accurate content reflects user inputs.

* chore: add eslint, knip, lint-staged

* chore: run prettier

* chore: minor improvements

* chore: add more test and improved e2e config

* minor fixes

* minor fixes

* chore: add new test
2025-03-27 21:41:55 +01:00

386 lines
12 KiB
TypeScript

"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { buyerSchema, type BuyerData } from "@/app/schema";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { CustomTooltip } from "@/components/ui/tooltip";
import { toast } from "sonner";
import { BUYERS_LOCAL_STORAGE_KEY } from "./buyer-management";
import { z } from "zod";
import { useState, useEffect } from "react";
import * as Sentry from "@sentry/nextjs";
const BUYER_FORM_ID = "buyer-form";
interface BuyerDialogProps {
isOpen: boolean;
onClose: React.Dispatch<React.SetStateAction<boolean>>;
handleBuyerAdd?: (
buyer: BuyerData,
{ shouldApplyNewBuyerToInvoice }: { shouldApplyNewBuyerToInvoice: boolean }
) => void;
handleBuyerEdit?: (buyer: BuyerData) => void;
initialData: BuyerData | null;
isEditMode: boolean;
formValues?: Partial<BuyerData>;
}
export function BuyerDialog({
isOpen,
onClose,
handleBuyerAdd,
handleBuyerEdit,
initialData,
isEditMode,
formValues,
}: BuyerDialogProps) {
const form = useForm<BuyerData>({
resolver: zodResolver(buyerSchema),
defaultValues: {
id: initialData?.id ?? "",
name: initialData?.name ?? "",
address: initialData?.address ?? "",
vatNo: initialData?.vatNo ?? "",
email: initialData?.email ?? "",
vatNoFieldIsVisible: initialData?.vatNoFieldIsVisible ?? true,
},
});
// by default, we want to apply the new buyer to the current invoice
const [shouldApplyNewBuyerToInvoice, setShouldApplyNewBuyerToInvoice] =
useState(true);
const [shouldApplyFormValues, setShouldApplyFormValues] = useState(false);
// Effect to update form values when switch is toggled
useEffect(() => {
// if the switch is on and we have form values, we want to apply the form values to the form
if (shouldApplyFormValues && formValues && !isEditMode) {
form.reset({
...form.getValues(),
...formValues,
});
}
// if the switch is off and we have initial data, we want to apply the initial data to the form
else if (!shouldApplyFormValues && !isEditMode) {
form.reset(
initialData ?? {
id: "",
name: "",
address: "",
vatNo: "",
email: "",
vatNoFieldIsVisible: true,
}
);
}
}, [shouldApplyFormValues, formValues, initialData, isEditMode, form]);
function onSubmit(formValues: BuyerData) {
try {
// **RUNNING SOME VALIDATIONS FIRST**
// Get existing buyers or initialize empty array
const buyers = localStorage.getItem(BUYERS_LOCAL_STORAGE_KEY);
const existingBuyers: unknown = buyers ? JSON.parse(buyers) : [];
// Validate existing buyers array with Zod
const existingBuyersValidationResult = z
.array(buyerSchema)
.safeParse(existingBuyers);
if (!existingBuyersValidationResult.success) {
console.error(
"Invalid existing buyers data:",
existingBuyersValidationResult.error
);
// Show error toast
toast.error("Error loading existing buyers", {
richColors: true,
description: "Please try again",
});
// Reset localStorage if validation fails
localStorage.setItem(BUYERS_LOCAL_STORAGE_KEY, JSON.stringify([]));
return;
}
// Validate buyer data against existing buyers
const isDuplicateName = existingBuyersValidationResult.data.some(
(buyer: BuyerData) =>
buyer.name === formValues.name && buyer.id !== formValues.id
);
if (isDuplicateName) {
form.setError("name", {
type: "manual",
message: "A buyer with this name already exists",
});
// Focus on the name input field for user to fix the error
form.setFocus("name");
// Show error toast
toast.error("A buyer with this name already exists", {
richColors: true,
});
return;
}
if (isEditMode) {
// Edit buyer
handleBuyerEdit?.(formValues);
} else {
// Add new buyer
handleBuyerAdd?.(formValues, { shouldApplyNewBuyerToInvoice });
}
// Close dialog
onClose(false);
// Reset form
form.reset();
} catch (error) {
console.error("Failed to save buyer:", error);
toast.error("Failed to save buyer", {
description: "Please try again",
richColors: true,
});
Sentry.captureException(error);
}
}
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) {
onClose(false);
form.reset();
}
}}
>
<DialogContent
className="flex flex-col gap-0 overflow-y-visible p-0 sm:max-w-lg [&>button:last-child]:top-3.5"
data-testid={`manage-buyer-dialog`}
>
<DialogHeader className="border-b border-slate-200 px-6 py-4 dark:border-slate-800">
<DialogTitle className="text-base">
{isEditMode ? "Edit Buyer" : "Add New Buyer"}
</DialogTitle>
<DialogDescription>
{isEditMode
? "Edit the buyer details"
: "Add a new buyer to use later in your invoices"}
</DialogDescription>
</DialogHeader>
<div className="overflow-y-auto px-6 py-4">
{/* Add Use Current Form Values switch */}
{!isEditMode && (
<div className="mb-4 flex items-center gap-2">
<Switch
checked={shouldApplyFormValues}
onCheckedChange={setShouldApplyFormValues}
id="apply-form-values-switch"
/>
<CustomTooltip
trigger={
<Label
htmlFor="apply-form-values-switch"
className="cursor-pointer"
>
Use Current Form Values
</Label>
}
content="Pre-fill with values from the current invoice form"
className="z-[1000]"
/>
</div>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id={BUYER_FORM_ID}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Textarea
{...field}
rows={3}
placeholder="Enter buyer name"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Address</FormLabel>
<FormControl>
<Textarea
{...field}
rows={3}
placeholder="Enter buyer address"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* VAT Number */}
<div className="space-y-4">
<div className="flex items-end justify-between">
<FormField
control={form.control}
name="vatNo"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>VAT Number</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter VAT number" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Show/Hide VAT Number Field in PDF */}
<div className="ml-4 flex items-center gap-2">
<FormField
control={form.control}
name="vatNoFieldIsVisible"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<Switch
checked={field.value}
onCheckedChange={field.onChange}
id="vatNoFieldIsVisible"
/>
<CustomTooltip
trigger={
<Label htmlFor="vatNoFieldIsVisible">
Show in PDF
</Label>
}
content='Show/Hide the "VAT Number" field in the PDF'
className="z-[1000]"
/>
</div>
</FormItem>
)}
/>
</div>
</div>
</div>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
{...field}
type="email"
placeholder="buyer@email.com"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
{/* Apply to Current Invoice switch remains at bottom */}
{!isEditMode && (
<div className="mt-4 flex items-center gap-2 border-t pt-4">
<Switch
checked={shouldApplyNewBuyerToInvoice}
onCheckedChange={setShouldApplyNewBuyerToInvoice}
id="apply-buyer-to-current-invoice-switch"
/>
<CustomTooltip
trigger={
<Label
htmlFor="apply-buyer-to-current-invoice-switch"
className="cursor-pointer"
>
Apply to Current Invoice
</Label>
}
content="When enabled, the newly created buyer will be automatically applied to your current invoice form"
className="z-[1000]"
/>
</div>
)}
</div>
<DialogFooter className="border-border border-t px-6 py-4">
<DialogClose asChild>
<Button type="button" _variant="outline">
Cancel
</Button>
</DialogClose>
<Button
type="button"
onClick={async () => {
// validate the form
const isValid = await form.trigger();
if (!isValid) return;
// submit the form
onSubmit(form.getValues());
}}
form={BUYER_FORM_ID}
>
Save Buyer
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}