diff --git a/web/app/components/billing/plan-upgrade-modal/index.spec.tsx b/web/app/components/billing/plan-upgrade-modal/index.spec.tsx
new file mode 100644
index 0000000000..324043d439
--- /dev/null
+++ b/web/app/components/billing/plan-upgrade-modal/index.spec.tsx
@@ -0,0 +1,118 @@
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import PlanUpgradeModal from './index'
+
+const mockSetShowPricingModal = jest.fn()
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}))
+
+jest.mock('@/app/components/base/modal', () => {
+ const MockModal = ({ isShow, children }: { isShow: boolean; children: React.ReactNode }) => (
+ isShow ?
{children}
: null
+ )
+ return {
+ __esModule: true,
+ default: MockModal,
+ }
+})
+
+jest.mock('@/context/modal-context', () => ({
+ useModalContext: () => ({
+ setShowPricingModal: mockSetShowPricingModal,
+ }),
+}))
+
+const baseProps = {
+ title: 'Upgrade Required',
+ description: 'You need to upgrade your plan.',
+ show: true,
+ onClose: jest.fn(),
+}
+
+const renderComponent = (props: Partial> = {}) => {
+ const mergedProps = { ...baseProps, ...props }
+ return render()
+}
+
+describe('PlanUpgradeModal', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ // Rendering and props-driven content
+ it('should render modal with provided content when visible', () => {
+ // Arrange
+ const extraInfoText = 'Additional upgrade details'
+ renderComponent({
+ extraInfo: {extraInfoText}
,
+ })
+
+ // Assert
+ expect(screen.getByText(baseProps.title)).toBeInTheDocument()
+ expect(screen.getByText(baseProps.description)).toBeInTheDocument()
+ expect(screen.getByText(extraInfoText)).toBeInTheDocument()
+ expect(screen.getByText('billing.triggerLimitModal.dismiss')).toBeInTheDocument()
+ expect(screen.getByText('billing.triggerLimitModal.upgrade')).toBeInTheDocument()
+ })
+
+ // Guard against rendering when modal is hidden
+ it('should not render content when show is false', () => {
+ // Act
+ renderComponent({ show: false })
+
+ // Assert
+ expect(screen.queryByText(baseProps.title)).not.toBeInTheDocument()
+ expect(screen.queryByText(baseProps.description)).not.toBeInTheDocument()
+ })
+
+ // User closes the modal from dismiss button
+ it('should call onClose when dismiss button is clicked', async () => {
+ // Arrange
+ const user = userEvent.setup()
+ const onClose = jest.fn()
+ renderComponent({ onClose })
+
+ // Act
+ await user.click(screen.getByText('billing.triggerLimitModal.dismiss'))
+
+ // Assert
+ expect(onClose).toHaveBeenCalledTimes(1)
+ })
+
+ // Upgrade path uses provided callback over pricing modal
+ it('should call onUpgrade and onClose when upgrade button is clicked with onUpgrade provided', async () => {
+ // Arrange
+ const user = userEvent.setup()
+ const onClose = jest.fn()
+ const onUpgrade = jest.fn()
+ renderComponent({ onClose, onUpgrade })
+
+ // Act
+ await user.click(screen.getByText('billing.triggerLimitModal.upgrade'))
+
+ // Assert
+ expect(onClose).toHaveBeenCalledTimes(1)
+ expect(onUpgrade).toHaveBeenCalledTimes(1)
+ expect(mockSetShowPricingModal).not.toHaveBeenCalled()
+ })
+
+ // Fallback upgrade path opens pricing modal when no onUpgrade is supplied
+ it('should open pricing modal when upgrade button is clicked without onUpgrade', async () => {
+ // Arrange
+ const user = userEvent.setup()
+ const onClose = jest.fn()
+ renderComponent({ onClose, onUpgrade: undefined })
+
+ // Act
+ await user.click(screen.getByText('billing.triggerLimitModal.upgrade'))
+
+ // Assert
+ expect(onClose).toHaveBeenCalledTimes(1)
+ expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
+ })
+})