Add end-to-end tests for proforma customer selection and persistence

- Implement tests to verify that the selected customer is retained when creating a proforma.
- Add functionality to change the customer and ensure the change is persisted.
- Include tests to confirm the customer change and refresh the recipient name.
- Test cancellation of customer change to ensure the previous customer remains selected.
This commit is contained in:
David Arranz 2026-09-15 11:37:50 +02:00
parent 0b086047ee
commit ba257fd489
31 changed files with 249 additions and 1772 deletions

View File

@ -98,7 +98,7 @@ export const CompanyDetailsSettingsPage = () => {
> >
<RdxPanel <RdxPanel
description={t("settings.companyDetails.sections.general.description")} description={t("settings.companyDetails.sections.general.description")}
icon={<ScrollTextIcon className="size-5" />} icon={<ScrollTextIcon className="size-6" />}
title={t("settings.companyDetails.sections.general.title")} title={t("settings.companyDetails.sections.general.title")}
variant="plain" variant="plain"
> >
@ -148,7 +148,7 @@ export const CompanyDetailsSettingsPage = () => {
<RdxPanel <RdxPanel
description={t("settings.companyDetails.sections.contact.description")} description={t("settings.companyDetails.sections.contact.description")}
icon={<ContactIcon className="size-5" />} icon={<ContactIcon className="size-6" />}
title={t("settings.companyDetails.sections.contact.title")} title={t("settings.companyDetails.sections.contact.title")}
variant="plain" variant="plain"
> >
@ -198,7 +198,7 @@ export const CompanyDetailsSettingsPage = () => {
</RdxPanel> </RdxPanel>
<RdxPanel <RdxPanel
description="Datos fiscales de la empresa" description="Datos fiscales de la empresa"
icon={<ContactIcon className="size-5" />} icon={<ContactIcon className="size-6" />}
title="Dirección fiscal" title="Dirección fiscal"
variant="plain" variant="plain"
> >
@ -238,7 +238,7 @@ export const CompanyDetailsSettingsPage = () => {
<RdxPanel <RdxPanel
description={t("settings.companyDetails.sections.paymentTerm.description")} description={t("settings.companyDetails.sections.paymentTerm.description")}
icon={<LandmarkIcon className="size-5" />} icon={<LandmarkIcon className="size-6" />}
title={t("settings.companyDetails.sections.paymentTerm.title")} title={t("settings.companyDetails.sections.paymentTerm.title")}
variant="plain" variant="plain"
> >

View File

@ -157,7 +157,7 @@ test("update registra customer y recipient en una sola mutación Audit", async (
city: Maybe.some(unwrap(City.create("Madrid"))), city: Maybe.some(unwrap(City.create("Madrid"))),
postalCode: Maybe.some(unwrap(PostalCode.create("28001"))), postalCode: Maybe.some(unwrap(PostalCode.create("28001"))),
province: Maybe.some(unwrap(Province.create("Madrid"))), province: Maybe.some(unwrap(Province.create("Madrid"))),
country: Maybe.some(unwrap(Country.create("ES"))), countryCode: Maybe.some(unwrap(Country.create("ES"))),
}, },
}), }),
} as never, } as never,

View File

@ -213,7 +213,7 @@ export class ProformaUpdater implements IProformaUpdater {
city: customer.address.city, city: customer.address.city,
postalCode: customer.address.postalCode, postalCode: customer.address.postalCode,
province: customer.address.province, province: customer.address.province,
country: customer.address.country, country: customer.address.countryCode,
}); });
if (recipientResult.isFailure) { if (recipientResult.isFailure) {

View File

@ -13,6 +13,7 @@ import { ArrowRightIcon, CheckIcon, LockIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
import { ProformaStatusBadge } from "../../list/ui/components";
import { PROFORMA_STATUS, type ProformaStatus } from "../../shared"; import { PROFORMA_STATUS, type ProformaStatus } from "../../shared";
import type { ChangeProformaStatusTarget } from "../entities"; import type { ChangeProformaStatusTarget } from "../entities";
import { import {
@ -49,7 +50,7 @@ const StatusBadge = ({ status }: { status: ProformaStatus }) => {
getProformaStatusColor(status) getProformaStatusColor(status)
)} )}
> >
<Icon className="size-3.5" /> <Icon className="size-" />
{t(`catalog.proformas.status.${status}.label`)} {t(`catalog.proformas.status.${status}.label`)}
</span> </span>
); );
@ -149,7 +150,7 @@ export const ChangeProformaStatusDialog = ({
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{t("proformas.change_proforma_status_dialog.current_status", "Estado actual")} {t("proformas.change_proforma_status_dialog.current_status", "Estado actual")}
</span> </span>
<StatusBadge status={currentTarget.status} /> <ProformaStatusBadge status={currentTarget.status} />
</div> </div>
{orderedAvailableStatuses.length === 0 ? ( {orderedAvailableStatuses.length === 0 ? (

View File

@ -44,7 +44,7 @@ export const ProformaCreateInitialInfoCard = ({
return ( return (
<RdxPanel <RdxPanel
icon={<FileTextIcon className="size-5" />} icon={<FileTextIcon className="size-6" />}
title={t("proformas.create.initial_info.title", "Información inicial")} title={t("proformas.create.initial_info.title", "Información inicial")}
variant="card" variant="card"
> >

View File

@ -52,7 +52,7 @@ export const ProformaTotalsSummary = ({
className={className} className={className}
description={t("proformas.update.totals.description")} description={t("proformas.update.totals.description")}
disabled={disabled} disabled={disabled}
icon={<CurrencyIcon className="size-5" />} icon={<CurrencyIcon className="size-6" />}
title={t("proformas.update.totals.title", "Totales")} title={t("proformas.update.totals.title", "Totales")}
> >
<FormSectionGrid className="gap-5 md:grid-cols-1"> <FormSectionGrid className="gap-5 md:grid-cols-1">

View File

@ -172,12 +172,21 @@ export const ProformaUpdateCustomerChangeDialog = ({
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<Button disabled={isSubmitting} onClick={() => onOpenChange(false)} variant="outline"> <Button
disabled={isSubmitting}
id="proforma-customer-change-cancel"
onClick={() => onOpenChange(false)}
variant="outline"
>
{t("proformas.update.customer_change.dialog.cancel", "Cancelar")} {t("proformas.update.customer_change.dialog.cancel", "Cancelar")}
</Button> </Button>
{status === "compatible" ? ( {status === "compatible" ? (
<Button disabled={isSubmitting} onClick={onConfirm}> <Button
disabled={isSubmitting}
id="proforma-customer-change-confirm"
onClick={onConfirm}
>
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Spinner aria-hidden className="mr-2 size-4" /> <Spinner aria-hidden className="mr-2 size-4" />

View File

@ -28,7 +28,7 @@ export const ProformaUpdatePaymentEditor = ({
<RdxPanel <RdxPanel
className={className} className={className}
description={t("form_groups.proformas.payment.description", "Forma de pago")} description={t("form_groups.proformas.payment.description", "Forma de pago")}
icon={<CreditCardIcon className="size-5" />} icon={<CreditCardIcon className="size-6" />}
title={t("form_groups.proformas.payment.title", "Condiciones de pago")} title={t("form_groups.proformas.payment.title", "Condiciones de pago")}
variant="card" variant="card"
> >

View File

@ -22,7 +22,7 @@ export const ProformaUpdateSettingsEditor = ({
className={className} className={className}
description={t("form_groups.proformas.settings.description", "Configuración de la proforma")} description={t("form_groups.proformas.settings.description", "Configuración de la proforma")}
disabled={disabled} disabled={disabled}
icon={<Settings2Icon className="size-5" />} icon={<Settings2Icon className="size-6" />}
title={t("form_groups.proformas.settings.title", "Configuración")} title={t("form_groups.proformas.settings.title", "Configuración")}
> >
<FormSectionGrid className="md:grid-cols-1"> <FormSectionGrid className="md:grid-cols-1">

View File

@ -31,7 +31,7 @@ export const ProformaUpdateHeaderEditor = ({
return ( return (
<RdxPanel <RdxPanel
className={className} className={className}
icon={<FileTextIcon className="size-5" />} icon={<FileTextIcon className="size-6" />}
title={t("form_groups.proformas.basic_info.title")} title={t("form_groups.proformas.basic_info.title")}
variant="card" variant="card"
> >

View File

@ -38,7 +38,7 @@ export const ProformaUpdateRecipientEditor = ({
"form_groups.proformas.customer.description", "form_groups.proformas.customer.description",
"Cliente destinatario de la proforma" "Cliente destinatario de la proforma"
)} )}
icon={<UserIcon className="size-5" />} icon={<UserIcon className="size-6" />}
title={t("form_groups.proformas.customer.title", "Cliente")} title={t("form_groups.proformas.customer.title", "Cliente")}
variant="card" variant="card"
> >
@ -60,6 +60,7 @@ export const ProformaUpdateRecipientEditor = ({
<Button <Button
className="justify-center" className="justify-center"
disabled={disabled} disabled={disabled}
id="proforma-change-customer"
onClick={onChangeCustomerClick} onClick={onChangeCustomerClick}
type="button" type="button"
variant={selectedCustomer ? "outline" : "default"} variant={selectedCustomer ? "outline" : "default"}

View File

@ -44,7 +44,7 @@ export const ProformaUpdateTaxEditor = ({
"form_groups.proformas.taxes.description", "form_groups.proformas.taxes.description",
"Configura los valores fiscales por defecto de la proforma. Los impuestos aplicados y los totales se calculan desde las líneas valoradas." "Configura los valores fiscales por defecto de la proforma. Los impuestos aplicados y los totales se calculan desde las líneas valoradas."
)} )}
icon={<LandmarkIcon className="size-5" />} icon={<LandmarkIcon className="size-6" />}
title={t("form_groups.proformas.taxes.title", "Configuración fiscal")} title={t("form_groups.proformas.taxes.title", "Configuración fiscal")}
variant="card" variant="card"
> >

View File

@ -30,7 +30,7 @@ export const CompanyVerifactuConfigurationSection = ({
return ( return (
<RdxPanel <RdxPanel
description={t("settings.verifactu.configuration.description")} description={t("settings.verifactu.configuration.description")}
icon={<ShieldCheckIcon className="size-5" />} icon={<ShieldCheckIcon className="size-6" />}
title={t("settings.verifactu.configuration.title")} title={t("settings.verifactu.configuration.title")}
variant="plain" variant="plain"
> >

View File

@ -19,7 +19,7 @@ export const CompanyVerifactuDefaultsSection = ({
return ( return (
<RdxPanel <RdxPanel
description={t("settings.verifactu.defaults.description")} description={t("settings.verifactu.defaults.description")}
icon={<LandmarkIcon className="size-5" />} icon={<LandmarkIcon className="size-6" />}
title={t("settings.verifactu.defaults.title")} title={t("settings.verifactu.defaults.title")}
variant="plain" variant="plain"
> >

View File

@ -21,7 +21,7 @@ export const CompanyVerifactuStatusSection = () => {
return ( return (
<RdxPanel <RdxPanel
description={t("settings.verifactu.status.description")} description={t("settings.verifactu.status.description")}
icon={<FileCogIcon className="size-5" />} icon={<FileCogIcon className="size-6" />}
title={t("settings.verifactu.status.title")} title={t("settings.verifactu.status.title")}
variant="plain" variant="plain"
> >

View File

@ -18,7 +18,7 @@ export const CustomerAdditionalConfigEditor = ({
return ( return (
<RdxPanel <RdxPanel
description={t("form_groups.preferences.description")} description={t("form_groups.preferences.description")}
icon={<Settings2Icon className="size-5" />} icon={<Settings2Icon className="size-6" />}
title={t("form_groups.preferences.title")} title={t("form_groups.preferences.title")}
variant="card" variant="card"
> >

View File

@ -18,7 +18,7 @@ export const CustomerAddressEditor = ({
return ( return (
<RdxPanel <RdxPanel
description={t("form_groups.address.description")} description={t("form_groups.address.description")}
icon={<MapPinIcon className="size-5" />} icon={<MapPinIcon className="size-6" />}
title={t("form_groups.address.title")} title={t("form_groups.address.title")}
variant="card" variant="card"
> >

View File

@ -34,7 +34,7 @@ export const CustomerBasicInfoEditor = ({
return ( return (
<RdxPanel <RdxPanel
description={t("form_groups.basic_info.description")} description={t("form_groups.basic_info.description")}
icon={<FileTextIcon className="size-5" />} icon={<FileTextIcon className="size-6" />}
title={t("form_groups.basic_info.title")} title={t("form_groups.basic_info.title")}
variant="card" variant="card"
> >

View File

@ -25,7 +25,7 @@ export const CustomerContactEditor = ({
return ( return (
<RdxPanel <RdxPanel
description={t("form_groups.contact_info.description")} description={t("form_groups.contact_info.description")}
icon={<PhoneIcon className="size-5" />} icon={<PhoneIcon className="size-6" />}
title={t("form_groups.contact_info.title")} title={t("form_groups.contact_info.title")}
variant="card" variant="card"
> >

View File

@ -29,7 +29,7 @@ export const CustomerFiscalEditor = ({
return ( return (
<RdxPanel <RdxPanel
description={t("form_groups.fiscal_info.description")} description={t("form_groups.fiscal_info.description")}
icon={<LandmarkIcon className="size-5" />} icon={<LandmarkIcon className="size-6" />}
title={t("form_groups.fiscal_info.title")} title={t("form_groups.fiscal_info.title")}
variant="card" variant="card"
> >

View File

@ -60,7 +60,7 @@ export const SupplierEditorForm = ({
<form className="flex min-w-0 flex-col gap-6" id={formId} noValidate onSubmit={onSubmit}> <form className="flex min-w-0 flex-col gap-6" id={formId} noValidate onSubmit={onSubmit}>
<RdxPanel <RdxPanel
description={t("form.sections.identity.description")} description={t("form.sections.identity.description")}
icon={<FileTextIcon className="size-5" />} icon={<FileTextIcon className="size-6" />}
title={t("form.sections.identity.title")} title={t("form.sections.identity.title")}
> >
<RdxFormGrid className="md:grid-cols-2"> <RdxFormGrid className="md:grid-cols-2">
@ -109,7 +109,7 @@ export const SupplierEditorForm = ({
<RdxPanel <RdxPanel
description={t("form.sections.address.description")} description={t("form.sections.address.description")}
icon={<MapPinIcon className="size-5" />} icon={<MapPinIcon className="size-6" />}
title={t("form.sections.address.title")} title={t("form.sections.address.title")}
> >
<RdxFormGrid className="md:grid-cols-2"> <RdxFormGrid className="md:grid-cols-2">
@ -129,7 +129,7 @@ export const SupplierEditorForm = ({
<RdxPanel <RdxPanel
description={t("form.sections.contact.description")} description={t("form.sections.contact.description")}
icon={<PhoneIcon className="size-5" />} icon={<PhoneIcon className="size-6" />}
title={t("form.sections.contact.title")} title={t("form.sections.contact.title")}
> >
<RdxFormGrid className="md:grid-cols-2"> <RdxFormGrid className="md:grid-cols-2">
@ -172,7 +172,7 @@ export const SupplierEditorForm = ({
<RdxPanel <RdxPanel
description={t("form.sections.preferences.description")} description={t("form.sections.preferences.description")}
icon={<LanguagesIcon className="size-5" />} icon={<LanguagesIcon className="size-6" />}
title={t("form.sections.preferences.title")} title={t("form.sections.preferences.title")}
> >
<RdxFormGrid className="md:grid-cols-2"> <RdxFormGrid className="md:grid-cols-2">

View File

@ -19,7 +19,9 @@
"./components": "./src/components/index.ts", "./components": "./src/components/index.ts",
"./components/*": "./src/components/*.tsx", "./components/*": "./src/components/*.tsx",
"./locales/*": "./src/locales/*", "./locales/*": "./src/locales/*",
"./hooks": ["./src/hooks/index.ts"] "./hooks": [
"./src/hooks/index.ts"
]
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.11", "@biomejs/biome": "^2.4.11",
@ -36,22 +38,22 @@
"esbuild-plugin-react18-css": "^0.0.4", "esbuild-plugin-react18-css": "^0.0.4",
"rimraf": "^6.1.3", "rimraf": "^6.1.3",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"tsup": "^8.5.1", "tsup": "^8.5.1",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^6.0.2", "typescript": "^6.0.2",
"typescript-plugin-css-modules": "^5.2.0", "typescript-plugin-css-modules": "^5.2.0",
"vite-tsconfig-paths": "^6.1.1" "vite-tsconfig-paths": "^6.1.1"
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.3.0", "@base-ui/react": "1.8.0",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@repo/i18next": "workspace:*", "@repo/i18next": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",

View File

@ -54,12 +54,12 @@ export const RdxPanel = ({
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex min-w-0 items-start gap-3"> <div className="flex min-w-0 items-start gap-3">
{icon ? ( {icon ? (
<div className="flex size-6 shrink-0 items-center justify-center text-primary"> <div className="flex size-8 shrink-0 items-center justify-center text-primary">
{icon} {icon}
</div> </div>
) : null} ) : null}
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-0.5">
<h2 <h2
className="min-w-0 text-base font-semibold tracking-tight text-foreground" className="min-w-0 text-base font-semibold tracking-tight text-foreground"
id={titleId} id={titleId}

View File

@ -393,7 +393,7 @@ export function PDFViewer({
contentClassName="space-y-3 px-2 sm:space-y-4 sm:px-4 md:space-y-5 md:px-6" contentClassName="space-y-3 px-2 sm:space-y-4 sm:px-4 md:space-y-5 md:px-6"
description={description} description={description}
headerClassName="px-2 sm:px-4 md:px-6" headerClassName="px-2 sm:px-4 md:px-6"
icon={<EyeIcon className="size-5" />} icon={<EyeIcon className="size-6" />}
title={title} title={title}
> >
{viewerContent} {viewerContent}

View File

@ -7,5 +7,5 @@ export const RdxBadge = ({
className, className,
...props ...props
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) => ( }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) => (
<Badge className={cn("tracking-wide px-4 h-6 rounded-md", className)} {...props} /> <Badge className={cn("tracking-wide px-3 h-6 rounded-lg", className)} {...props} />
); );

View File

@ -5,7 +5,7 @@
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "", "config": "",
"css": "@repo/shadcn-ui/globals.css", "css": "src/styles/globals.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""

View File

@ -39,11 +39,10 @@
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.3.0", "@base-ui/react": "^1.8.0",
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist": "^5.2.8",
"@fontsource-variable/geist-mono": "^5.2.7", "@fontsource-variable/geist-mono": "^5.2.7",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-toggle-group": "^1.1.11",
"@shadcn/react": "^0.3.1", "@shadcn/react": "^0.3.1",
"add": "^2.0.6", "add": "^2.0.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -57,7 +56,6 @@
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pnpm": "^10.33.0", "pnpm": "^10.33.0",
"radix-ui": "latest",
"react-day-picker": "10.0.1", "react-day-picker": "10.0.1",
"react-hook-form": "^7.72.1", "react-hook-form": "^7.72.1",
"react-resizable-panels": "^4.9.0", "react-resizable-panels": "^4.9.0",
@ -66,7 +64,6 @@
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^4.3.6" "zod": "^4.3.6"
} }
} }

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { Label } from "@repo/shadcn-ui/components/label"; import { useRender } from "@base-ui/react/use-render";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import * as React from "react"; import * as React from "react";
import { import {
@ -82,39 +82,55 @@ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
); );
FormItem.displayName = "FormItem"; FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<HTMLLabelElement, useRender.ComponentProps<"label">>(
React.ElementRef<typeof LabelPrimitive.Root>, ({ className, render, ...props }, ref) => {
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> const { error, formItemId } = useFormField();
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return ( return useRender({
<Label defaultTagName: "label",
className={cn(error && "text-destructive", className)} props: {
htmlFor={formItemId} className: cn(
ref={ref} "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
{...props} error && "text-destructive",
/> className
); ),
}); ...props,
htmlFor: formItemId,
},
ref,
render,
state: {
slot: "label",
},
});
}
);
FormLabel.displayName = "FormLabel"; FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef< type FormControlProps = Omit<useRender.ComponentProps<"div">, "children" | "render"> & {
React.ElementRef<typeof Slot>, children: React.ReactElement;
React.ComponentPropsWithoutRef<typeof Slot> };
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return ( const FormControl = React.forwardRef<HTMLElement, FormControlProps>(
<Slot ({ children, ...props }, ref) => {
aria-describedby={error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`} const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
aria-invalid={!!error}
id={formItemId} return useRender({
ref={ref} defaultTagName: "div",
{...props} props: {
/> "aria-describedby": error ? `${formDescriptionId} ${formMessageId}` : formDescriptionId,
); "aria-invalid": !!error,
}); id: formItemId,
...props,
},
ref,
render: children,
state: {
slot: "form-control",
},
});
}
);
FormControl.displayName = "FormControl"; FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<

View File

@ -6,6 +6,7 @@
@import "@fontsource-variable/geist-mono"; @import "@fontsource-variable/geist-mono";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@source "../../../../apps/**/*.{ts,tsx}"; @source "../../../../apps/**/*.{ts,tsx}";
@source "../../../../modules/**/*.{ts,tsx}"; @source "../../../../modules/**/*.{ts,tsx}";
@source "../../../../packages/**/*.{ts,tsx}"; @source "../../../../packages/**/*.{ts,tsx}";
@ -66,7 +67,7 @@
--font-serif: "Geist Variable", serif; --font-serif: "Geist Variable", serif;
--font-mono: "Geist Mono Variable", monospace; --font-mono: "Geist Mono Variable", monospace;
--radius: 0.625rem; --radius: 0.45rem;
--shadow-2xs: 0 1px 3px 0px rgb(0 0 0 / 0.05); --shadow-2xs: 0 1px 3px 0px rgb(0 0 0 / 0.05);
--shadow-xs: 0 1px 3px 0px rgb(0 0 0 / 0.05); --shadow-xs: 0 1px 3px 0px rgb(0 0 0 / 0.05);
@ -185,10 +186,13 @@
--font-mono: var(--font-mono); --font-mono: var(--font-mono);
--font-serif: var(--font-serif); --font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
--shadow-2xs: var(--shadow-2xs); --shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs); --shadow-xs: var(--shadow-xs);
@ -212,4 +216,8 @@
[role="button"]:not(:disabled) { [role="button"]:not(:disabled) {
cursor: pointer; cursor: pointer;
} }
html {
@apply font-sans;
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
import { randomUUID } from "node:crypto";
import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { createCustomerViaUi } from "../../customers/support/create-customer-via-ui";
import { loginViaUi } from "../../support/login-via-ui";
import { createProformaViaUi } from "./support/create-proforma-via-ui";
const selectCandidate = async (page: Page, customer: { id: string; search: string }) => {
await page.locator("#proforma-change-customer").click();
const responsePromise = page.waitForResponse((response) => {
const url = new URL(response.url());
return (
response.request().method() === "GET" &&
/\/customers\/?$/.test(url.pathname) &&
url.searchParams.get("q") === customer.search
);
});
await page.getByRole("dialog").locator('input[inputmode="search"]').fill(customer.search);
const response = await responsePromise;
expect(response.status()).toBe(200);
await page.getByTestId(`customer-selection-${customer.id}`).click();
await page.locator("#customer-selection-confirm").click();
};
const confirmCandidate = async (page: Page) => {
await expect(page.locator("#proforma-customer-change-confirm")).toBeVisible();
await page.locator("#proforma-customer-change-confirm").click();
};
const save = async (page: Page, id: string) => {
const response = page.waitForResponse(
(value) =>
value.request().method() === "PUT" &&
new URL(value.url()).pathname.endsWith(`/proformas/${id}`)
);
await page.getByTestId("proforma-update-submit").click();
const result = await response;
expect(result.status()).toBe(200);
};
const returnToProforma = async (page: Page, id: string) => {
await page.goto(`/proformas/${id}/edit?returnTo=/proformas`);
await expect(page.locator("#proforma-change-customer")).toBeVisible();
};
test("CUSTOMER-INVOICES-PROFORMA-CUSTOMER-001: conserva el cliente seleccionado al crear", async ({
page,
}, testInfo) => {
await loginViaUi(page);
const proforma = await createProformaViaUi(page, testInfo);
await page.reload();
await expect(page.locator("#proforma-recipient-name")).toHaveText(proforma.customer.name);
});
test("CUSTOMER-INVOICES-PROFORMA-CUSTOMER-002: cambia de cliente y lo persiste", async ({
page,
}, testInfo) => {
await loginViaUi(page);
const proforma = await createProformaViaUi(page, testInfo);
const search = randomUUID().replaceAll("-", "");
const customer = await createCustomerViaUi(page, testInfo, `E2E Customer B ${search}`);
await returnToProforma(page, proforma.id);
await selectCandidate(page, { id: customer.id, search });
await confirmCandidate(page);
await expect(page.locator("#proforma-recipient-name")).toHaveText(customer.name);
await save(page, proforma.id);
await page.reload();
await expect(page.locator("#proforma-recipient-name")).toHaveText(customer.name);
});
test("CUSTOMER-INVOICES-PROFORMA-CUSTOMER-003: confirma el cambio y refresca el recipient", async ({
page,
}, testInfo) => {
await loginViaUi(page);
const proforma = await createProformaViaUi(page, testInfo);
const search = randomUUID().replaceAll("-", "");
const customer = await createCustomerViaUi(page, testInfo, `E2E Customer Derived ${search}`);
await returnToProforma(page, proforma.id);
await selectCandidate(page, { id: customer.id, search });
await confirmCandidate(page);
await expect(page.locator("#proforma-recipient-name")).toHaveText(customer.name);
await save(page, proforma.id);
await page.reload();
await expect(page.locator("#proforma-recipient-name")).toHaveText(customer.name);
});
test("CUSTOMER-INVOICES-PROFORMA-CUSTOMER-004: cancelar conserva el cliente anterior", async ({
page,
}, testInfo) => {
await loginViaUi(page);
const proforma = await createProformaViaUi(page, testInfo);
const search = randomUUID().replaceAll("-", "");
const customer = await createCustomerViaUi(page, testInfo, `E2E Customer Cancel ${search}`);
await returnToProforma(page, proforma.id);
await selectCandidate(page, { id: customer.id, search });
await expect(page.locator("#proforma-customer-change-cancel")).toBeVisible();
await page.locator("#proforma-customer-change-cancel").click();
await expect(page.locator("#proforma-recipient-name")).toHaveText(proforma.customer.name);
await page.reload();
await expect(page.locator("#proforma-recipient-name")).toHaveText(proforma.customer.name);
});