diff --git a/.github/screenshots/easy-invoice-logo.svg b/.github/screenshots/easy-invoice-logo.svg deleted file mode 100644 index e2346d7..0000000 --- a/.github/screenshots/easy-invoice-logo.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..87a39d3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,106 @@ +# Changelog + +## [1.0.3] - 2026-03-29 + +### Added + +- Email visibility toggle for seller and buyer sections — control whether the email address appears in the generated PDF +- `ConfirmDiscardDialog` component to warn users about unsaved changes when closing the buyer/seller dialogs (replaces native `window.confirm`) +- `useConfirmDiscard` reusable hook for managing discard confirmation state across buyer and seller dialogs +- Knip CI workflow for automated dead-code and unused-dependency detection +- `update-github-actions` script in `package.json` to streamline GitHub Actions version updates +- Unit tests for the `generate-invoice` API route and core logic (`generate-invoice.ts`, `route.tsx`) + +### Changed + +- Refactored `generate-invoice` API route: extracted core business logic into a standalone `generate-invoice.ts` module using a dependency-injection pattern for improved testability +- Reworked seller and buyer information form sections with improved layout, locked-state banners, and cleaner field grouping +- Buyer and seller dialogs now reset form values and pre-fill switch to their defaults when closed +- Buyer and seller names are trimmed of whitespace before saving; whitespace-padded duplicates are rejected +- Invalid localStorage entries for buyers and sellers are now validated and silently dropped instead of causing errors +- Out-of-Date dates helper improved with more accurate state detection +- Error message component layout and copy updated for better readability +- Vitest config updated with JSX support (`esbuild.jsx: "automatic"`) to enable unit-testing JSX components +- Restructured buyer and seller dialog components into dedicated feature directories under `sections/components/buyer` and `sections/components/seller` +- GitHub Actions workflows updated to latest action versions; failure handling added to all CI jobs +- Auto-scroll the invoice form on mobile when switching between tabs + +### Fixed + +- Pre-fill switch in buyer/seller dialogs no longer retains its state after the dialog is closed and reopened +- Rate limit exceeded log upgraded from `console.log` to `console.error` for correct severity +- Loading placeholder display fixed when switching invoice tabs on mobile + +## [1.0.2] - 2026-03-10 + +### Added + +- QR code generation for invoices with customizable descriptions and visibility toggles, supported in both default and Stripe templates +- Logo upload for the default invoice template (previously available only in the Stripe template) +- Searchable currency combobox with grouped categories, replacing the native dropdown for faster selection +- Improved multi-page PDF support with automatic pagination and page breaks + +### Changed + +- Increased QR code size and improved rendering quality for better scannability +- Enhanced invoice template text color and visuals for improved readability +- Reorganized Stripe payment link input position in the form for better flow +- Improved user feedback during invoice item deletion with better toast notification handling +- Enhanced error handling to reset invoice metadata to defaults on errors +- Clearer error messages when invoice sharing fails +- Tooltip on the "Add invoice item" button for contextual guidance +- Sentry error tracking integration for invoice sharing and GitHub stars features + +### Fixed + +- i18n issue when generating PDF via the API route +- Delete invoice item flow not working correctly +- Item name field validation too strict (now optional for flexibility) + +## [1.0.1] - 2026-01-12 + +### Added + +- Stripe-inspired invoice template with professional styling and layout optimizations +- Dynamic template selection in the invoice form +- Logo upload capability for the Stripe template with validation +- Stripe payment URL field for enhanced invoice functionality +- Customizable Tax/VAT label text (e.g., "VAT", "GST", "Sales Tax") +- Customizable Tax Number label in buyer and seller information sections +- Dynamic tax label updates based on selected invoice language + +### Changed + +- Landing page cleanup: refined About section and footer for better layout and accessibility +- Call-to-action toasts: added custom, randomized CTA toasts encouraging user support +- Added support for more currencies with improved date handling +- Enhanced tooltips with detailed explanations and improved styling +- Enhanced validation for VAT input to accept both numeric values and specific strings +- Improved user interface messages for clarity regarding VAT input requirements + +### Fixed + +- Bug with accordion component +- Error message for invoice link generation now includes a refresh suggestion + +## [1.0.0] - 2025-11-19 + +### Added + +- Initial release of EasyInvoicePDF — a free, open-source invoice generator +- Live preview: invoice updates in real-time as you make changes +- Shareable links: generate secure links to share invoices directly with clients +- Instant PDF download with one click +- Multi-language support (English, Polish, German, Spanish, Portuguese, Russian, Ukrainian, French, Italian, Dutch) +- Support for all major currencies with automatic locale-based formatting +- European VAT calculation and formatting compliant with EU tax requirements +- Complete seller and buyer information management with save-for-future-use +- Detailed invoice items with descriptions, quantities, and pricing +- Automatic tax calculations and totals +- Invoice numbering, dating, and payment terms +- No sign-up required — fully browser-based with no server-side data storage + +[1.0.3]: https://github.com/VladSez/easy-invoice-pdf/compare/v1.0.2...v1.0.3 +[1.0.2]: https://github.com/VladSez/easy-invoice-pdf/compare/EasyInvoicePDF-1.0.1...v1.0.2 +[1.0.1]: https://github.com/VladSez/easy-invoice-pdf/compare/EasyInvoicePDF-v1.0.0...EasyInvoicePDF-1.0.1 +[1.0.0]: https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0 diff --git a/README.md b/README.md index 11f5966..a432b68 100644 --- a/README.md +++ b/README.md @@ -94,16 +94,52 @@ _**Choose between multiple professional templates** (Default and Stripe) to matc +## 📢 What's New + +**v1.0.3 — Seller & Buyer Improvements (Mar 2026)** + +- **Email visibility toggle** — control whether email addresses appear in the generated PDF +- **Confirm discard dialog** — warns about unsaved changes when closing buyer/seller dialogs +- **Improved seller & buyer forms** — reworked layout, locked-state banners, and cleaner field grouping + +[Full release notes for v1.0.3](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.3) + +--- + +**v1.0.2 — QR Codes & Multi-Page PDFs (Mar 2026)** + +- **QR code support** — add payment QR codes with custom descriptions to both templates +- **Logo upload for default template** — previously available only in the Stripe template +- **Searchable currency combobox** — grouped categories replace the native dropdown +- **Improved multi-page PDFs** — automatic pagination and page breaks for large invoices + +[Full release notes for v1.0.2](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.2) + +--- + +**v1.0.1 — Stripe Template & Tax Customization (Jan 2026)** + +- **Stripe-inspired invoice template** — professional styling with logo upload and payment URL field +- **Customizable tax labels** — set VAT, GST, Sales Tax, or any custom label per invoice language +- **Improved i18n** — dynamic tax label updates and better locale-based currency handling + +[Full release notes for v1.0.1](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1) + +--- + +**v1.0.0 — Initial Release (Nov 2025)** + +- **Live preview** — invoice updates in real-time as you type +- **Instant PDF download** — one-click, no sign-up required +- **Shareable links** — send invoices directly to clients without attachments +- **10 languages & 120+ currencies** — full multi-language and currency support out of the box + +[Full release notes for v1.0.0](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0) + ## 🌟 Star History [![RepoStars](https://repostars.dev/api/embed?repo=VladSez%2Feasy-invoice-pdf&theme=dark)](https://repostars.dev/?repos=VladSez%2Feasy-invoice-pdf&theme=dark) -## 📢 News & Updates - -- **Mar 10, 2026**: Added QR code support, logo upload for the default template, searchable currency combobox, and improved multi-page PDF support. [Release notes for v1.0.2](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.2) -- **Jan 11, 2026**: Added customizable tax/VAT labels, improved internationalization (i18n) translations, enhanced overall performance, and fixed multiple bugs. [Release notes for v1.0.1](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1) -- **Nov 19, 2025**: EasyInvoicePDF version 1.0.0 released! Create professional invoices in seconds. Welcome to try EasyInvoicePDF. [Release notes for v1.0.0](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0) - ## 🎥 Demo Video Watch a quick demo of EasyInvoicePDF in action to see how easy it is to create professional invoices in seconds. The video demonstrates key features like **Live Preview**, **Instant PDF Download**, and **Customization Options**. diff --git a/e2e/buyer.test.ts b/e2e/buyer.test.ts index 6449e79..93fcfd0 100644 --- a/e2e/buyer.test.ts +++ b/e2e/buyer.test.ts @@ -1099,61 +1099,4 @@ test.describe("Buyer management", () => { expect(parsedData).toHaveLength(1); expect(parsedData[0].name).toBe("Globex Corp"); }); - - test("drops invalid localStorage entries and preserves valid ones on save", async ({ - page, - }) => { - // Seed: one valid + one corrupt buyer (component already mounted with empty localStorage, - // so "New Buyer" button is still visible — onSubmit reads localStorage fresh) - await page.addInitScript(() => { - const buyers = [ - { - id: "pre-seeded-valid", - name: "Pre-seeded Valid Buyer", - address: "1 Valid Avenue", - email: "valid@pre-seeded.com", - emailFieldIsVisible: true, - vatNo: "VATPRE", - vatNoLabelText: "Tax Number", - vatNoFieldIsVisible: true, - notes: "", - notesFieldIsVisible: true, - }, - { corrupt: true, notABuyer: 42 }, - ]; - localStorage.setItem("EASY_INVOICE_PDF_BUYERS", JSON.stringify(buyers)); - }); - - await page.goto("/?template=default"); - await expect(page).toHaveURL("/?template=default"); - - await page.getByRole("button", { name: "New Buyer" }).click(); - - const manageBuyerDialog = page.getByTestId("manage-buyer-dialog"); - await manageBuyerDialog - .getByRole("textbox", { name: "Name" }) - .fill("Brand New Buyer"); - await manageBuyerDialog - .getByRole("textbox", { name: "Address" }) - .fill("99 Fresh Boulevard"); - - await manageBuyerDialog.getByRole("button", { name: "Save Buyer" }).click(); - - // Submit succeeds — no error toast - await expect( - page.getByText("Buyer added and applied to invoice", { exact: true }), - ).toBeVisible(); - - // localStorage: valid entry preserved, corrupt entry dropped, new entry added - const storedData = (await page.evaluate(() => - localStorage.getItem("EASY_INVOICE_PDF_BUYERS"), - )) as string; - const parsedData = JSON.parse(storedData) as BuyerData[]; - - expect(parsedData).toHaveLength(2); - expect(parsedData.some((b) => b.name === "Pre-seeded Valid Buyer")).toBe( - true, - ); - expect(parsedData.some((b) => b.name === "Brand New Buyer")).toBe(true); - }); }); diff --git a/e2e/default-invoice-template/default-invoice-template.test.ts b/e2e/default-invoice-template/default-invoice-template.test.ts index a4775ac..7e982b5 100644 --- a/e2e/default-invoice-template/default-invoice-template.test.ts +++ b/e2e/default-invoice-template/default-invoice-template.test.ts @@ -2,10 +2,7 @@ import { INITIAL_INVOICE_DATA } from "@/app/constants"; import { INVOICE_PDF_TRANSLATIONS } from "@/app/(app)/pdf-i18n-translations/pdf-translations"; import fs from "node:fs"; import path from "node:path"; -import { - SMALL_TEST_IMAGE_BASE64, - uploadBase64LogoAsFile, -} from "../stripe-invoice-template/utils"; +import { uploadLogoFile } from "../stripe-invoice-template/utils"; // IMPORTANT: we use custom extended test fixture that provides a temporary download directory for each test import { test, expect } from "../utils/extended-playwright-test"; @@ -19,7 +16,7 @@ test.describe("Default Invoice Template", () => { // we set the system time to a fixed date, so that the invoice number and other dates are consistent across tests await page.clock.setSystemTime(new Date("2025-12-17T00:00:00Z")); - await page.goto("/"); + await page.goto("/?template=default"); await expect(page).toHaveURL("/?template=default"); }); @@ -1579,7 +1576,7 @@ test.describe("Default Invoice Template", () => { const generalInfoSection = page.getByTestId("general-information-section"); // Upload a valid logo - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Verify logo preview is visible await expect(page.getByText("Logo uploaded successfully!")).toBeVisible(); diff --git a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Desktop-Chrome-darwin.png b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Desktop-Chrome-darwin.png index de2de30..6798c7b 100644 Binary files a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Desktop-Chrome-darwin.png and b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Desktop-Chrome-darwin.png differ diff --git a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Mobile-Chrome-darwin.png b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Mobile-Chrome-darwin.png index de2de30..80c9f11 100644 Binary files a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Mobile-Chrome-darwin.png and b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Mobile-Chrome-darwin.png differ diff --git a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Mobile-Safari-darwin.png b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Mobile-Safari-darwin.png index 3e28d55..a8d9edf 100644 Binary files a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Mobile-Safari-darwin.png and b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-default-template-Mobile-Safari-darwin.png differ diff --git a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Desktop-Chrome-darwin.png b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Desktop-Chrome-darwin.png index a04fcb6..e07c78d 100644 Binary files a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Desktop-Chrome-darwin.png and b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Desktop-Chrome-darwin.png differ diff --git a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Mobile-Chrome-darwin.png b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Mobile-Chrome-darwin.png index a04fcb6..e07c78d 100644 Binary files a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Mobile-Chrome-darwin.png and b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Mobile-Chrome-darwin.png differ diff --git a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Mobile-Safari-darwin.png b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Mobile-Safari-darwin.png index 8cfa033..e6fac2d 100644 Binary files a/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Mobile-Safari-darwin.png and b/e2e/default-invoice-template/default-invoice-template.test.ts-snapshots/pdf-with-logo-stripe-template-after-switch-Mobile-Safari-darwin.png differ diff --git a/e2e/fixtures/app-logo.png b/e2e/fixtures/app-logo.png new file mode 100644 index 0000000..244b5ac Binary files /dev/null and b/e2e/fixtures/app-logo.png differ diff --git a/e2e/seller.test.ts b/e2e/seller.test.ts index 6da2fb8..3bec405 100644 --- a/e2e/seller.test.ts +++ b/e2e/seller.test.ts @@ -1165,67 +1165,4 @@ test.describe("Seller management", () => { expect(parsedData).toHaveLength(1); expect(parsedData[0].name).toBe("Acme Corp"); }); - - test("drops invalid localStorage entries and preserves valid ones on save", async ({ - page, - }) => { - // Seed: one valid + one corrupt seller (component already mounted with empty localStorage, - // so "New Seller" button is still visible — onSubmit reads localStorage fresh) - await page.evaluate(() => { - const sellers = [ - { - id: "pre-seeded-valid", - name: "Pre-seeded Valid Seller", - address: "1 Valid Road", - email: "valid@pre-seeded.com", - emailFieldIsVisible: true, - vatNo: "VATPRE", - vatNoLabelText: "Tax Number", - vatNoFieldIsVisible: true, - accountNumber: "ACCTPRE", - accountNumberFieldIsVisible: true, - swiftBic: "SWIFTPRE", - swiftBicFieldIsVisible: true, - notes: "", - notesFieldIsVisible: true, - }, - { corrupt: true, notASeller: 42 }, - ]; - localStorage.setItem("EASY_INVOICE_PDF_SELLERS", JSON.stringify(sellers)); - }); - - await page.goto("/?template=default"); - await expect(page).toHaveURL("/?template=default"); - - await page.getByRole("button", { name: "New Seller" }).click(); - - const manageSellerDialog = page.getByTestId("manage-seller-dialog"); - await manageSellerDialog - .getByRole("textbox", { name: "Name" }) - .fill("Brand New Seller"); - await manageSellerDialog - .getByRole("textbox", { name: "Address" }) - .fill("99 Fresh Lane"); - - await manageSellerDialog - .getByRole("button", { name: "Save Seller" }) - .click(); - - // Submit succeeds — no error toast - await expect( - page.getByText("Seller added and applied to invoice", { exact: true }), - ).toBeVisible(); - - // localStorage: valid entry preserved, corrupt entry dropped, new entry added - const storedData = (await page.evaluate(() => - localStorage.getItem("EASY_INVOICE_PDF_SELLERS"), - )) as string; - const parsedData = JSON.parse(storedData) as SellerData[]; - - expect(parsedData).toHaveLength(2); - expect(parsedData.some((s) => s.name === "Pre-seeded Valid Seller")).toBe( - true, - ); - expect(parsedData.some((s) => s.name === "Brand New Seller")).toBe(true); - }); }); diff --git a/e2e/stripe-invoice-template/share-logic.test.ts b/e2e/stripe-invoice-template/share-logic.test.ts index f02c7b0..fd333c1 100644 --- a/e2e/stripe-invoice-template/share-logic.test.ts +++ b/e2e/stripe-invoice-template/share-logic.test.ts @@ -1,10 +1,10 @@ import { PDF_DATA_LOCAL_STORAGE_KEY, type InvoiceData } from "@/app/schema"; import { expect, test } from "@playwright/test"; -import { SMALL_TEST_IMAGE_BASE64, uploadBase64LogoAsFile } from "./utils"; +import { uploadLogoFile } from "./utils"; test.describe("Stripe Invoice Sharing Logic", () => { test.beforeEach(async ({ page }) => { - await page.goto("/"); + await page.goto("/?template=default"); }); test("can share invoice with Stripe template and *WITHOUT* logo", async ({ @@ -67,13 +67,13 @@ test.describe("Stripe Invoice Sharing Logic", () => { // Verify logo upload section is visible (but empty since no logo was shared) await expect( - newPageGeneralInfoSection.getByText("Company Logo (Optional)"), + newPageGeneralInfoSection.getByText("Company Logo", { exact: true }), ).toBeVisible(); // Verify payment URL section is visible await expect( newPageGeneralInfoSection.getByRole("textbox", { - name: "Payment Link URL (Optional)", + name: "Payment Link URL", }), ).toBeVisible(); @@ -109,7 +109,7 @@ test.describe("Stripe Invoice Sharing Logic", () => { .selectOption("stripe"); // Upload logo - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Wait for logo to be uploaded const generalInfoSection = page.getByTestId("general-information-section"); @@ -148,7 +148,7 @@ test.describe("Stripe Invoice Sharing Logic", () => { .selectOption("stripe"); // Upload logo - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Wait debounce timeout // eslint-disable-next-line playwright/no-wait-for-timeout @@ -223,7 +223,7 @@ test.describe("Stripe Invoice Sharing Logic", () => { .selectOption("stripe"); // Upload logo - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Verify share button becomes disabled await expect(shareButton).toHaveAttribute("data-disabled", "true"); @@ -259,7 +259,7 @@ test.describe("Stripe Invoice Sharing Logic", () => { await page.waitForURL("/?template=stripe"); // Upload logo - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Wait for upload and verify share button is disabled const generalInfoSection = page.getByTestId("general-information-section"); @@ -287,9 +287,7 @@ test.describe("Stripe Invoice Sharing Logic", () => { const parsedData = JSON.parse(storedData) as InvoiceData; - expect(parsedData).toMatchObject({ - logo: SMALL_TEST_IMAGE_BASE64, - } satisfies Pick); + expect(parsedData.logo).toBeTruthy(); // Reload the page await page.reload(); @@ -330,7 +328,7 @@ test.describe("Stripe Invoice Sharing Logic", () => { .selectOption("stripe"); // Upload logo - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Wait for logo to be uploaded // eslint-disable-next-line playwright/no-wait-for-timeout diff --git a/e2e/stripe-invoice-template/stripe-invoice-template.test.ts b/e2e/stripe-invoice-template/stripe-invoice-template.test.ts index ea37bdd..747f190 100644 --- a/e2e/stripe-invoice-template/stripe-invoice-template.test.ts +++ b/e2e/stripe-invoice-template/stripe-invoice-template.test.ts @@ -6,7 +6,7 @@ import { } from "@/app/schema"; import fs from "node:fs"; import path from "node:path"; -import { SMALL_TEST_IMAGE_BASE64, uploadBase64LogoAsFile } from "./utils"; +import { uploadLogoFile } from "./utils"; // IMPORTANT: we use custom extended test fixture that provides a temporary download directory for each test import { expect, test } from "../utils/extended-playwright-test"; @@ -21,7 +21,7 @@ test.describe("Stripe Invoice Template", () => { // we set the system time to a fixed date, so that the invoice number and other dates are consistent across tests await page.clock.setSystemTime(new Date("2025-12-17T00:00:00Z")); - await page.goto("/"); + await page.goto("/?template=default"); }); test("displays correct OG meta tags for Stripe template", async ({ @@ -83,7 +83,7 @@ test.describe("Stripe Invoice Template", () => { // Logo section is visible on default template await expect( - generalInfoSection.getByText("Company Logo (Optional)"), + generalInfoSection.getByText("Company Logo", { exact: true }), ).toBeVisible(); await expect( generalInfoSection.getByTestId("logo-upload-input"), @@ -92,7 +92,7 @@ test.describe("Stripe Invoice Template", () => { // Payment URL section should not be visible on default template await expect( generalInfoSection.getByRole("textbox", { - name: "Payment Link URL (Optional)", + name: "Payment Link URL", }), ).toBeHidden(); @@ -112,7 +112,7 @@ test.describe("Stripe Invoice Template", () => { ).toBeVisible(); await expect( - generalInfoSection.getByText("Company Logo (Optional)"), + generalInfoSection.getByText("Company Logo", { exact: true }), ).toBeVisible(); await expect( generalInfoSection.getByText("Click to upload your company logo"), @@ -124,7 +124,7 @@ test.describe("Stripe Invoice Template", () => { // Payment URL section should now be visible on Stripe template await expect( generalInfoSection.getByRole("textbox", { - name: "Payment Link URL (Optional)", + name: "Payment Link URL", }), ).toBeVisible(); @@ -135,7 +135,7 @@ test.describe("Stripe Invoice Template", () => { // Logo section remains visible on default template await expect( - generalInfoSection.getByText("Company Logo (Optional)"), + generalInfoSection.getByText("Company Logo", { exact: true }), ).toBeVisible(); await expect( @@ -145,7 +145,7 @@ test.describe("Stripe Invoice Template", () => { // Payment URL section should be hidden again on default template await expect( generalInfoSection.getByRole("textbox", { - name: "Payment Link URL (Optional)", + name: "Payment Link URL", }), ).toBeHidden(); }); @@ -226,7 +226,7 @@ test.describe("Stripe Invoice Template", () => { const generalInfoSection = page.getByTestId("general-information-section"); // Upload a valid small image - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Should show success toast await expect(page.getByText("Logo uploaded successfully!")).toBeVisible(); @@ -259,7 +259,7 @@ test.describe("Stripe Invoice Template", () => { .selectOption("stripe"); // Upload a valid small image - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); const generalInfoSection = page.getByTestId("general-information-section"); @@ -304,7 +304,7 @@ test.describe("Stripe Invoice Template", () => { const generalInfoSection = page.getByTestId("general-information-section"); const paymentUrlInput = generalInfoSection.getByRole("textbox", { - name: "Payment Link URL (Optional)", + name: "Payment Link URL", }); // Try invalid URL @@ -337,11 +337,11 @@ test.describe("Stripe Invoice Template", () => { // Add payment URL await generalInfoSection - .getByRole("textbox", { name: "Payment Link URL (Optional)" }) + .getByRole("textbox", { name: "Payment Link URL" }) .fill("https://buy.stripe.com/test_payment_link"); // Upload logo - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Wait for logo to be uploaded and PDF to regenerate await expect(page.getByText("Logo uploaded successfully!")).toBeVisible(); @@ -369,9 +369,7 @@ test.describe("Stripe Invoice Template", () => { const parsedData = JSON.parse(storedData) as InvoiceData; - expect(parsedData).toMatchObject({ - logo: SMALL_TEST_IMAGE_BASE64, - } satisfies Pick); + expect(parsedData.logo).toBeTruthy(); // Reload page await page.reload(); @@ -386,7 +384,7 @@ test.describe("Stripe Invoice Template", () => { // Verify payment URL persists await expect( generalInfoSection.getByRole("textbox", { - name: "Payment Link URL (Optional)", + name: "Payment Link URL", }), ).toHaveValue("https://buy.stripe.com/test_payment_link"); @@ -900,7 +898,7 @@ test.describe("Stripe Invoice Template", () => { await expect(page).toHaveURL("/?template=stripe"); // Upload a valid logo - await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64); + await uploadLogoFile(page); // Wait for logo to be uploaded and PDF to regenerate await expect(page.getByText("Logo uploaded successfully!")).toBeVisible(); @@ -917,7 +915,7 @@ test.describe("Stripe Invoice Template", () => { // Add payment URL await generalInfoSection - .getByRole("textbox", { name: "Payment Link URL (Optional)" }) + .getByRole("textbox", { name: "Payment Link URL" }) .fill("https://buy.stripe.com/test_payment_link"); // Wait a moment for any debounced localStorage updates @@ -933,9 +931,7 @@ test.describe("Stripe Invoice Template", () => { const parsedData = JSON.parse(storedData) as InvoiceData; - expect(parsedData).toMatchObject({ - logo: SMALL_TEST_IMAGE_BASE64, - } satisfies Pick); + expect(parsedData.logo).toBeTruthy(); const downloadPDFButton = page.getByRole("link", { name: "Download PDF in English", diff --git a/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Desktop-Chrome-darwin.png b/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Desktop-Chrome-darwin.png index e84d26b..e8db905 100644 Binary files a/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Desktop-Chrome-darwin.png and b/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Desktop-Chrome-darwin.png differ diff --git a/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Mobile-Chrome-darwin.png b/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Mobile-Chrome-darwin.png index e84d26b..c6d3a86 100644 Binary files a/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Mobile-Chrome-darwin.png and b/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Mobile-Chrome-darwin.png differ diff --git a/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Mobile-Safari-darwin.png b/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Mobile-Safari-darwin.png index aa2b82d..8e15d55 100644 Binary files a/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Mobile-Safari-darwin.png and b/e2e/stripe-invoice-template/stripe-invoice-template.test.ts-snapshots/pdf-with-logo-and-payment-url-when-using-stripe-template-Mobile-Safari-darwin.png differ diff --git a/e2e/stripe-invoice-template/utils.ts b/e2e/stripe-invoice-template/utils.ts index cbfb54a..ed0bbbc 100644 --- a/e2e/stripe-invoice-template/utils.ts +++ b/e2e/stripe-invoice-template/utils.ts @@ -1,51 +1,8 @@ -/** - * Create a small test image in base64 format (1x1 pixel PNG) - */ -export const SMALL_TEST_IMAGE_BASE64 = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="; +import path from "node:path"; +import type { Page } from "@playwright/test"; -/** - * Upload a base64 image to the logo input - */ -export const uploadBase64LogoAsFile = (base64Data: string) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - const fileInput = document.querySelector( - "#logoUpload", - ) as HTMLInputElement | null; +const LOGO_FIXTURE_PATH = path.join(__dirname, "../fixtures/app-logo.png"); - if (!fileInput) { - throw new Error('Logo upload input "#logoUpload" was not found'); - } - - if (!base64Data) { - throw new Error("Base64 data is required"); - } - - // Convert base64 data to binary string by removing metadata and decoding - const byteString = atob(base64Data.split(",")[1]); - - // Extract MIME type from base64 metadata (e.g. "image/png") - const mimeString = base64Data.split(",")[0].split(":")[1].split(";")[0]; - - // Create ArrayBuffer to store binary data - const ab = new ArrayBuffer(byteString.length); - - // Create Uint8Array view to write bytes to ArrayBuffer - const ia = new Uint8Array(ab); - - // Convert binary string to bytes and write to array - for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); - } - - // Create File object from ArrayBuffer with name and MIME type - const file = new File([ab], "test-logo.png", { type: mimeString }); - - // Create DataTransfer to simulate file input - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - - // Set file input's files property and trigger change event - fileInput.files = dataTransfer.files; - fileInput.dispatchEvent(new Event("change", { bubbles: true })); -}; +export async function uploadLogoFile(page: Page) { + await page.setInputFiles("#logoUpload", LOGO_FIXTURE_PATH); +} diff --git a/package.json b/package.json index 6b8dd50..18563bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pdf-invoice-generator", - "version": "1.0.2", + "version": "1.0.3", "private": true, "packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a", "pnpm": { diff --git a/src/app/(app)/components/invoice-form/sections/general-information.tsx b/src/app/(app)/components/invoice-form/sections/general-information.tsx index 063f1df..ea6d1ff 100644 --- a/src/app/(app)/components/invoice-form/sections/general-information.tsx +++ b/src/app/(app)/components/invoice-form/sections/general-information.tsx @@ -656,7 +656,7 @@ export const GeneralInformation = memo(function GeneralInformation({ {/* Logo Upload */}
{logo ? ( @@ -718,7 +718,7 @@ export const GeneralInformation = memo(function GeneralInformation({ {template === "stripe" && (
+ +--- + +### Changed + +- Invalid localStorage entries for buyers and sellers are now validated and silently dropped instead of causing errors +- Error message component layout and copy updated for better readability +- GitHub Actions workflows updated to latest action versions; failure handling added to all CI jobs +- Added knip GitHub CI job for automated dead-code and unused-dependency detection + +### Fixed + +- Pre-fill switch in buyer/seller dialogs no longer retains its state after the dialog is closed and reopened +- Buyer and seller dialogs now reset form values and pre-fill switch to their defaults when closed +- Buyer and seller names are trimmed of whitespace before saving; whitespace-padded duplicates are rejected + +### What's Changed + +- feat: auto-scroll form on mobile when switching between tabs, fix loading placeholder by @VladSez in [#191](https://github.com/VladSez/easy-invoice-pdf/pull/191) +- refactor: update layout and improve accessibility in invoice-related components by @VladSez in [#192](https://github.com/VladSez/easy-invoice-pdf/pull/192) +- refactor: improve layout in BuyerDialog and SellerDialog components by @VladSez in [#193](https://github.com/VladSez/easy-invoice-pdf/pull/193) +- refactor: streamline toast management in InvoiceForm component by @VladSez in [#194](https://github.com/VladSez/easy-invoice-pdf/pull/194) +- feat: enhance buyer and seller management with new functionality and improved toast notifications by @VladSez in [#195](https://github.com/VladSez/easy-invoice-pdf/pull/195) +- feat: reworked seller/buyer sections, email visibility switch field, shared invoice indicator, improved Out-of-Date helper + minor things by @VladSez in [#197](https://github.com/VladSez/easy-invoice-pdf/pull/197) +- feat: add cancel confirm dialog for seller/buyer dialog, update gh actions, added new script update-github-actions in package.json by @VladSez in [#198](https://github.com/VladSez/easy-invoice-pdf/pull/198) +- fix: update GitHub Actions script and fix buyer/seller dialog pre-fill bug by @VladSez in [#199](https://github.com/VladSez/easy-invoice-pdf/pull/199) +- feat: replaced window.confirm with alert discard dialog for buyer and seller management by @VladSez in [#200](https://github.com/VladSez/easy-invoice-pdf/pull/200) + +--- + +**Full Changelog**: [`v1.0.2...v1.0.3`](https://github.com/VladSez/easy-invoice-pdf/compare/v1.0.2...v1.0.3) + +[View Release Notes for v1.0.3 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.3) diff --git a/src/app/changelog/content/stripe-pdf-template.mdx b/src/app/changelog/content/stripe-pdf-template.mdx index c8027a1..ff0d594 100644 --- a/src/app/changelog/content/stripe-pdf-template.mdx +++ b/src/app/changelog/content/stripe-pdf-template.mdx @@ -45,3 +45,5 @@ export const metadata = { --- **Full Changeset**: [`63b097b...7ae733b`](https://github.com/VladSez/easy-invoice-pdf/compare/63b097b...7ae733b) + +[View Release Notes for v1.0.1 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1)