From e94997c09f87abb26272b81da555395751a3c34b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Jan 2026 00:13:52 +0000 Subject: [PATCH 1/4] chore: sync from main [skip ci] From 0f56d4e9938eddb1d9e5d6b02a093d2caf8b145b Mon Sep 17 00:00:00 2001 From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:17:15 +0000 Subject: [PATCH 2/4] fix: sonarqube detected issues (#54) fix: sonarqube detected issues --- eslint.config.js | 2 +- src/components/SEO.jsx | 1 + src/components/layout/Navbar.jsx | 13 +++++---- src/components/ui/Carousel.jsx | 45 ++++++++++++++++++++------------ src/components/ui/Form.jsx | 14 ++++++---- src/main.jsx | 4 +-- src/pages/ContactPage.jsx | 6 ++--- src/pages/FAQPage.jsx | 12 ++++----- src/pages/HomePage.jsx | 2 +- src/pages/NotFoundPage.jsx | 4 +-- src/pages/PortfolioPage.jsx | 6 ++--- src/pages/ServicesPage.jsx | 18 ++++++------- 12 files changed, 72 insertions(+), 55 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b8b8b7d..d89d552 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,7 @@ import reactRefresh from "eslint-plugin-react-refresh"; import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - globalIgnores(["dist"]), + globalIgnores(["dist", "coverage"]), { files: ["**/*.{js,jsx}"], extends: [ diff --git a/src/components/SEO.jsx b/src/components/SEO.jsx index dc7a651..af73b86 100644 --- a/src/components/SEO.jsx +++ b/src/components/SEO.jsx @@ -60,4 +60,5 @@ SEO.propTypes = { ogType: PropTypes.string, }; +export { SEO }; export default SEO; diff --git a/src/components/layout/Navbar.jsx b/src/components/layout/Navbar.jsx index cf9ba67..ac65c8f 100644 --- a/src/components/layout/Navbar.jsx +++ b/src/components/layout/Navbar.jsx @@ -112,7 +112,7 @@ export const Navbar = React.forwardRef( ref={combinedRef} style={{ colorScheme: "only light" }} className={cn( - "sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur @supports-[backdrop-filter]:bg-background/60 px-4 md:px-6 [&_*]:no-underline", + "sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur @supports-[backdrop-filter]:bg-background/60 px-4 md:px-6 **:no-underline", className )} {...props} @@ -138,8 +138,11 @@ export const Navbar = React.forwardRef( - {navigationLinks.map((link, index) => ( - + {navigationLinks.map((link) => ( + - {navigationLinks.map((link, index) => ( - + {navigationLinks.map((link) => ( + ({ + carouselRef, + api: api, + opts, + orientation: + orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), + scrollPrev, + scrollNext, + canScrollPrev, + canScrollNext, + }), + [ + carouselRef, + api, + opts, + orientation, + scrollPrev, + scrollNext, + canScrollPrev, + canScrollNext, + ] + ); + return ( - -
+
{children} -
+
); } @@ -134,7 +146,6 @@ function CarouselItem({ className, ...props }) { return (
{ + const contextValue = React.useMemo( + () => ({ name: props.name }), + [props.name] + ); + return ( - + ); @@ -51,9 +56,10 @@ const FormItemContext = React.createContext({}); function FormItem({ className, ...props }) { const id = React.useId(); + const contextValue = React.useMemo(() => ({ id }), [id]); return ( - +
{ @@ -44,10 +44,10 @@ const FAQPage = () => { collapsible className="border rounded-lg px-4" > - {category.questions.map((item, index) => ( + {category.questions.map((item) => ( {item.question} @@ -78,9 +78,9 @@ const FAQPage = () => {
- {mythsAndFacts.map((item, index) => ( + {mythsAndFacts.map((item) => (
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index c541062..8b7e3a7 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -2,7 +2,7 @@ import { Link } from "react-router-dom"; import { Button } from "@/components/ui/Button"; import { Badge } from "@/components/ui/Badge"; import { CTASection } from "@/components/ui/CTASection"; -import SEO from "@/components/SEO"; +import { SEO } from "@/components/SEO"; import { Award, Users, ThermometerSun } from "lucide-react"; const heroImage = new URL("/hero-work.jpg", import.meta.url).href; diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx index c30d8da..5223d5a 100644 --- a/src/pages/NotFoundPage.jsx +++ b/src/pages/NotFoundPage.jsx @@ -1,7 +1,7 @@ import { Link } from "react-router-dom"; import { Button } from "@/components/ui/Button"; import { BarsScaleFadeIcon } from "@/components/ui/icons/BarsScaleFadeIcon"; -import SEO from "@/components/SEO"; +import { SEO } from "@/components/SEO"; import { useEffect, useState } from "react"; const NotFoundPage = () => { @@ -10,7 +10,7 @@ const NotFoundPage = () => { useEffect(() => { if (count <= 0) { - window.location.replace(basename); + globalThis.location.replace(basename); return; } diff --git a/src/pages/PortfolioPage.jsx b/src/pages/PortfolioPage.jsx index 915fe92..e014300 100644 --- a/src/pages/PortfolioPage.jsx +++ b/src/pages/PortfolioPage.jsx @@ -9,7 +9,7 @@ import { import { CTASection } from "@/components/ui/CTASection"; import Lightbox from "@/components/portfolio/Lightbox"; import SocialLinks from "@/components/portfolio/SocialLinks"; -import SEO from "@/components/SEO"; +import { SEO } from "@/components/SEO"; import { portfolioImages } from "@/data/portfolioImages"; const PortfolioPage = () => { @@ -86,9 +86,9 @@ const PortfolioPage = () => { {/* Dot indicators */}
- {portfolioImages.map((_, index) => ( + {portfolioImages.map((project, index) => ( + /> - + {/* Close button */} + - + {/* Previous button */} + -
- { -
+ {/* Next button */} + - + + {/* Image container */} +
+ { +
+
); }; diff --git a/src/components/ui/Lightbox.test.jsx b/src/components/ui/Lightbox.test.jsx new file mode 100644 index 0000000..53d362f --- /dev/null +++ b/src/components/ui/Lightbox.test.jsx @@ -0,0 +1,210 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import Lightbox from "./Lightbox"; + +const mockImages = [ + { image: "/image1.jpg", alt: "Image 1" }, + { image: "/image2.jpg", alt: "Image 2" }, + { image: "/image3.jpg", alt: "Image 3" }, +]; + +describe("Lightbox", () => { + let onCloseMock; + let onNavigateMock; + + beforeEach(() => { + onCloseMock = vi.fn(); + onNavigateMock = vi.fn(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + const renderLightbox = (currentIndex = 0) => { + return render( + + ); + }; + + it("renders the current image", () => { + renderLightbox(0); + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", "/image1.jpg"); + expect(img).toHaveAttribute("alt", "Image 1"); + }); + + it("renders fallback alt text when alt is not provided", () => { + const imagesWithoutAlt = [{ image: "/image1.jpg" }]; + render( + + ); + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("alt", "Projeto 1 de 1"); + }); + + it("renders navigation buttons", () => { + renderLightbox(); + expect(screen.getByLabelText("Foto anterior")).toBeInTheDocument(); + expect(screen.getByLabelText("Próxima foto")).toBeInTheDocument(); + expect(screen.getByLabelText("Fechar")).toBeInTheDocument(); + }); + + it("calls onClose when close button is clicked", () => { + renderLightbox(); + fireEvent.click(screen.getByLabelText("Fechar")); + expect(onCloseMock).toHaveBeenCalled(); + }); + + it("calls onClose when backdrop is clicked", () => { + renderLightbox(); + fireEvent.click(screen.getByLabelText("Fechar lightbox")); + expect(onCloseMock).toHaveBeenCalled(); + }); + + it("navigates to previous image when prev button is clicked", () => { + renderLightbox(1); + fireEvent.click(screen.getByLabelText("Foto anterior")); + expect(onNavigateMock).toHaveBeenCalledWith(0); + }); + + it("navigates to next image when next button is clicked", () => { + renderLightbox(1); + fireEvent.click(screen.getByLabelText("Próxima foto")); + expect(onNavigateMock).toHaveBeenCalledWith(2); + }); + + it("wraps to last image when navigating prev from first image", () => { + renderLightbox(0); + fireEvent.click(screen.getByLabelText("Foto anterior")); + expect(onNavigateMock).toHaveBeenCalledWith(2); // Last image index + }); + + it("wraps to first image when navigating next from last image", () => { + renderLightbox(2); + fireEvent.click(screen.getByLabelText("Próxima foto")); + expect(onNavigateMock).toHaveBeenCalledWith(0); // First image index + }); + + describe("keyboard navigation", () => { + it("closes lightbox on Escape key", () => { + renderLightbox(); + fireEvent.keyDown(document, { key: "Escape" }); + expect(onCloseMock).toHaveBeenCalled(); + }); + + it("navigates to previous image on ArrowLeft key", () => { + renderLightbox(1); + fireEvent.keyDown(document, { key: "ArrowLeft" }); + expect(onNavigateMock).toHaveBeenCalledWith(0); + }); + + it("navigates to next image on ArrowRight key", () => { + renderLightbox(1); + fireEvent.keyDown(document, { key: "ArrowRight" }); + expect(onNavigateMock).toHaveBeenCalledWith(2); + }); + + it("does not respond to other keys", () => { + renderLightbox(); + fireEvent.keyDown(document, { key: "Enter" }); + expect(onCloseMock).not.toHaveBeenCalled(); + expect(onNavigateMock).not.toHaveBeenCalled(); + }); + }); + + describe("touch/swipe navigation", () => { + it("navigates to next image on swipe left", () => { + renderLightbox(1); + const backdrop = screen.getByLabelText("Fechar lightbox"); + + fireEvent.touchStart(backdrop, { + touches: [{ clientX: 300 }], + }); + fireEvent.touchEnd(backdrop, { + changedTouches: [{ clientX: 100 }], + }); + + expect(onNavigateMock).toHaveBeenCalledWith(2); + }); + + it("navigates to previous image on swipe right", () => { + renderLightbox(1); + const backdrop = screen.getByLabelText("Fechar lightbox"); + + fireEvent.touchStart(backdrop, { + touches: [{ clientX: 100 }], + }); + fireEvent.touchEnd(backdrop, { + changedTouches: [{ clientX: 300 }], + }); + + expect(onNavigateMock).toHaveBeenCalledWith(0); + }); + + it("does not navigate on small swipe", () => { + renderLightbox(1); + const backdrop = screen.getByLabelText("Fechar lightbox"); + + fireEvent.touchStart(backdrop, { + touches: [{ clientX: 200 }], + }); + fireEvent.touchEnd(backdrop, { + changedTouches: [{ clientX: 180 }], + }); + + expect(onNavigateMock).not.toHaveBeenCalled(); + }); + + it("does not navigate when touchStart was not recorded", () => { + renderLightbox(1); + const backdrop = screen.getByLabelText("Fechar lightbox"); + + // Only fire touchEnd without touchStart + fireEvent.touchEnd(backdrop, { + changedTouches: [{ clientX: 100 }], + }); + + expect(onNavigateMock).not.toHaveBeenCalled(); + }); + }); + + it("stops propagation when clicking navigation buttons", () => { + renderLightbox(); + + // Click prev button - should not trigger backdrop close + fireEvent.click(screen.getByLabelText("Foto anterior")); + expect(onNavigateMock).toHaveBeenCalled(); + // onClose should not be called from the backdrop click + expect(onCloseMock).toHaveBeenCalledTimes(0); + + onNavigateMock.mockClear(); + + // Click next button - should not trigger backdrop close + fireEvent.click(screen.getByLabelText("Próxima foto")); + expect(onNavigateMock).toHaveBeenCalled(); + expect(onCloseMock).toHaveBeenCalledTimes(0); + }); + + it("cleans up keyboard event listener on unmount", () => { + const removeEventListenerSpy = vi.spyOn(document, "removeEventListener"); + const { unmount } = renderLightbox(); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith( + "keydown", + expect.any(Function) + ); + }); +}); diff --git a/src/pages/ContactPage.jsx b/src/pages/ContactPage.jsx index cc4b46e..d4faa9d 100644 --- a/src/pages/ContactPage.jsx +++ b/src/pages/ContactPage.jsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Textarea } from "@/components/ui/Textarea"; import ContactErrorDialog from "@/components/contact/ContactErrorDialog"; -import { SEO } from "@/components/SEO"; +import { SEO } from "@/components/common/SEO"; import { Form, FormControl, diff --git a/src/pages/FAQPage.jsx b/src/pages/FAQPage.jsx index f4aec2f..55128c0 100644 --- a/src/pages/FAQPage.jsx +++ b/src/pages/FAQPage.jsx @@ -1,11 +1,11 @@ -import { CTASection } from "@/components/ui/CTASection"; +import { CTASection } from "@/components/common/CTASection"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; -import { SEO } from "@/components/SEO"; +import { SEO } from "@/components/common/SEO"; import { faqCategories, mythsAndFacts } from "@/data/faqData"; const FAQPage = () => { diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index 8b7e3a7..354bafe 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -1,8 +1,8 @@ import { Link } from "react-router-dom"; import { Button } from "@/components/ui/Button"; import { Badge } from "@/components/ui/Badge"; -import { CTASection } from "@/components/ui/CTASection"; -import { SEO } from "@/components/SEO"; +import { CTASection } from "@/components/common/CTASection"; +import { SEO } from "@/components/common/SEO"; import { Award, Users, ThermometerSun } from "lucide-react"; const heroImage = new URL("/hero-work.jpg", import.meta.url).href; diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx index 5223d5a..34eb50c 100644 --- a/src/pages/NotFoundPage.jsx +++ b/src/pages/NotFoundPage.jsx @@ -1,7 +1,7 @@ import { Link } from "react-router-dom"; import { Button } from "@/components/ui/Button"; import { BarsScaleFadeIcon } from "@/components/ui/icons/BarsScaleFadeIcon"; -import { SEO } from "@/components/SEO"; +import { SEO } from "@/components/common/SEO"; import { useEffect, useState } from "react"; const NotFoundPage = () => { diff --git a/src/pages/PortfolioPage.jsx b/src/pages/PortfolioPage.jsx index e014300..aa9a1f6 100644 --- a/src/pages/PortfolioPage.jsx +++ b/src/pages/PortfolioPage.jsx @@ -6,10 +6,10 @@ import { CarouselPrevious, CarouselNext, } from "@/components/ui/Carousel"; -import { CTASection } from "@/components/ui/CTASection"; -import Lightbox from "@/components/portfolio/Lightbox"; -import SocialLinks from "@/components/portfolio/SocialLinks"; -import { SEO } from "@/components/SEO"; +import { CTASection } from "@/components/common/CTASection"; +import Lightbox from "@/components/ui/Lightbox"; +import SocialLinks from "@/components/common/SocialLinks"; +import { SEO } from "@/components/common/SEO"; import { portfolioImages } from "@/data/portfolioImages"; const PortfolioPage = () => { diff --git a/src/pages/ServicesPage.jsx b/src/pages/ServicesPage.jsx index 3180974..bda7003 100644 --- a/src/pages/ServicesPage.jsx +++ b/src/pages/ServicesPage.jsx @@ -1,5 +1,5 @@ -import { CTASection } from "@/components/ui/CTASection"; -import { SEO } from "@/components/SEO"; +import { CTASection } from "@/components/common/CTASection"; +import { SEO } from "@/components/common/SEO"; import { services, eticsBenefits, processSteps } from "@/data/servicesData"; const ServicesPage = () => {