diff --git a/web/app/components/billing/plan/assets/enterprise.spec.tsx b/web/app/components/billing/plan/assets/enterprise.spec.tsx
new file mode 100644
index 0000000000..831370f5d9
--- /dev/null
+++ b/web/app/components/billing/plan/assets/enterprise.spec.tsx
@@ -0,0 +1,249 @@
+import { render } from '@testing-library/react'
+import Enterprise from './enterprise'
+
+describe('Enterprise Icon Component', () => {
+ describe('Rendering', () => {
+ it('should render without crashing', () => {
+ const { container } = render()
+ expect(container.firstChild).toBeInTheDocument()
+ })
+
+ it('should render an SVG element', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('should have correct SVG attributes', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toHaveAttribute('xmlns', 'http://www.w3.org/2000/svg')
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ expect(svg).toHaveAttribute('viewBox', '0 0 32 32')
+ expect(svg).toHaveAttribute('fill', 'none')
+ })
+
+ it('should render only path elements', () => {
+ const { container } = render()
+ const paths = container.querySelectorAll('path')
+ const rects = container.querySelectorAll('rect')
+
+ // Enterprise icon uses only path elements, no rects
+ expect(paths.length).toBeGreaterThan(0)
+ expect(rects).toHaveLength(0)
+ })
+
+ it('should render elements with correct fill colors', () => {
+ const { container } = render()
+ const blueElements = container.querySelectorAll('[fill="var(--color-saas-dify-blue-inverted)"]')
+ const quaternaryElements = container.querySelectorAll('[fill="var(--color-text-quaternary)"]')
+
+ expect(blueElements.length).toBeGreaterThan(0)
+ expect(quaternaryElements.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('Component Behavior', () => {
+ it('should render consistently across multiple renders', () => {
+ const { container: container1 } = render()
+ const { container: container2 } = render()
+
+ expect(container1.innerHTML).toBe(container2.innerHTML)
+ })
+
+ it('should maintain stable output without memoization', () => {
+ const { container, rerender } = render()
+ const firstRender = container.innerHTML
+
+ rerender()
+ const secondRender = container.innerHTML
+
+ expect(firstRender).toBe(secondRender)
+ })
+
+ it('should be a functional component', () => {
+ expect(typeof Enterprise).toBe('function')
+ })
+ })
+
+ describe('Accessibility', () => {
+ it('should render as a decorative image', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('should be usable in accessible contexts', () => {
+ const { container } = render(
+
+
+
,
+ )
+
+ const wrapper = container.querySelector('[role="img"]')
+ expect(wrapper).toBeInTheDocument()
+ expect(wrapper).toHaveAttribute('aria-label', 'Enterprise plan')
+ })
+
+ it('should support custom wrapper accessibility', () => {
+ const { container } = render(
+ ,
+ )
+
+ const button = container.querySelector('button')
+ expect(button).toHaveAttribute('aria-label', 'Select Enterprise plan')
+ })
+ })
+
+ describe('Edge Cases', () => {
+ it('should handle multiple instances without conflicts', () => {
+ const { container } = render(
+ <>
+
+
+
+ >,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(3)
+ })
+
+ it('should maintain structure when wrapped in other elements', () => {
+ const { container } = render(
+
+
+
+
+
,
+ )
+
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ expect(svg?.getAttribute('width')).toBe('32')
+ })
+
+ it('should render correctly in grid layout', () => {
+ const { container } = render(
+
+
+
,
+ )
+
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('should render correctly in flex layout', () => {
+ const { container } = render(
+
+
+
,
+ )
+
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+ })
+
+ describe('CSS Variables', () => {
+ it('should use CSS custom properties for colors', () => {
+ const { container } = render()
+ const elementsWithCSSVars = container.querySelectorAll('[fill*="var("]')
+
+ expect(elementsWithCSSVars.length).toBeGreaterThan(0)
+ })
+
+ it('should have opacity attributes on quaternary path elements', () => {
+ const { container } = render()
+ const quaternaryPaths = container.querySelectorAll('path[fill="var(--color-text-quaternary)"]')
+
+ quaternaryPaths.forEach((path) => {
+ expect(path).toHaveAttribute('opacity', '0.18')
+ })
+ })
+
+ it('should not have opacity on blue inverted path elements', () => {
+ const { container } = render()
+ const bluePaths = container.querySelectorAll('path[fill="var(--color-saas-dify-blue-inverted)"]')
+
+ bluePaths.forEach((path) => {
+ expect(path).not.toHaveAttribute('opacity')
+ })
+ })
+
+ it('should use correct CSS variable names', () => {
+ const { container } = render()
+ const paths = container.querySelectorAll('path')
+
+ paths.forEach((path) => {
+ const fill = path.getAttribute('fill')
+ if (fill?.includes('var('))
+ expect(fill).toMatch(/var\(--(color-saas-dify-blue-inverted|color-text-quaternary)\)/)
+ })
+ })
+ })
+
+ describe('SVG Structure', () => {
+ it('should have correct path element structure', () => {
+ const { container } = render()
+ const paths = container.querySelectorAll('path')
+
+ paths.forEach((path) => {
+ expect(path).toHaveAttribute('d')
+ expect(path).toHaveAttribute('fill')
+ })
+ })
+
+ it('should have valid path data', () => {
+ const { container } = render()
+ const paths = container.querySelectorAll('path')
+
+ paths.forEach((path) => {
+ const d = path.getAttribute('d')
+ expect(d).toBeTruthy()
+ expect(d?.length).toBeGreaterThan(0)
+ })
+ })
+
+ it('should maintain proper element count', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg?.childNodes.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('Export', () => {
+ it('should be the default export', () => {
+ expect(Enterprise).toBeDefined()
+ expect(typeof Enterprise).toBe('function')
+ })
+
+ it('should return valid JSX', () => {
+ const result = Enterprise()
+ expect(result).toBeTruthy()
+ expect(result.type).toBe('svg')
+ })
+ })
+
+ describe('Performance', () => {
+ it('should render efficiently for multiple instances', () => {
+ const { container } = render(
+
+ {Array.from({ length: 10 }).map((_, i) => (
+
+ ))}
+
,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(10)
+ })
+ })
+})
diff --git a/web/app/components/billing/plan/assets/index.spec.tsx b/web/app/components/billing/plan/assets/index.spec.tsx
new file mode 100644
index 0000000000..b9b8763fb0
--- /dev/null
+++ b/web/app/components/billing/plan/assets/index.spec.tsx
@@ -0,0 +1,312 @@
+import { render } from '@testing-library/react'
+import { Enterprise, Professional, Sandbox, Team } from './index'
+
+// Import real components for comparison
+import SandboxDirect from './sandbox'
+import ProfessionalDirect from './professional'
+import TeamDirect from './team'
+import EnterpriseDirect from './enterprise'
+
+describe('Billing Plan Assets - Integration Tests', () => {
+ describe('Exports', () => {
+ it('should export Sandbox component', () => {
+ expect(Sandbox).toBeDefined()
+ // Sandbox is wrapped with React.memo, so it's an object
+ expect(typeof Sandbox).toMatch(/function|object/)
+ })
+
+ it('should export Professional component', () => {
+ expect(Professional).toBeDefined()
+ expect(typeof Professional).toBe('function')
+ })
+
+ it('should export Team component', () => {
+ expect(Team).toBeDefined()
+ expect(typeof Team).toBe('function')
+ })
+
+ it('should export Enterprise component', () => {
+ expect(Enterprise).toBeDefined()
+ expect(typeof Enterprise).toBe('function')
+ })
+
+ it('should export all four components', () => {
+ const exports = { Sandbox, Professional, Team, Enterprise }
+ expect(Object.keys(exports)).toHaveLength(4)
+ })
+ })
+
+ describe('Export Integrity', () => {
+ it('should export the correct Sandbox component', () => {
+ expect(Sandbox).toBe(SandboxDirect)
+ })
+
+ it('should export the correct Professional component', () => {
+ expect(Professional).toBe(ProfessionalDirect)
+ })
+
+ it('should export the correct Team component', () => {
+ expect(Team).toBe(TeamDirect)
+ })
+
+ it('should export the correct Enterprise component', () => {
+ expect(Enterprise).toBe(EnterpriseDirect)
+ })
+ })
+
+ describe('Rendering Integration', () => {
+ it('should render all components without conflicts', () => {
+ const { container } = render(
+ ,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(4)
+ })
+
+ it('should render Sandbox component correctly', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toBeInTheDocument()
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ })
+
+ it('should render Professional component correctly', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toBeInTheDocument()
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ })
+
+ it('should render Team component correctly', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toBeInTheDocument()
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ })
+
+ it('should render Enterprise component correctly', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toBeInTheDocument()
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ })
+ })
+
+ describe('Visual Consistency', () => {
+ it('should maintain consistent SVG dimensions across all components', () => {
+ const components = [
+ ,
+ ,
+ ,
+ ,
+ ]
+
+ components.forEach((component) => {
+ const { container } = render(component)
+ const svg = container.querySelector('svg')
+
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ expect(svg).toHaveAttribute('viewBox', '0 0 32 32')
+ })
+ })
+
+ it('should use consistent color variables across all components', () => {
+ const components = [Sandbox, Professional, Team, Enterprise]
+
+ components.forEach((Component) => {
+ const { container } = render()
+ const elementsWithBlue = container.querySelectorAll('[fill="var(--color-saas-dify-blue-inverted)"]')
+ const elementsWithQuaternary = container.querySelectorAll('[fill="var(--color-text-quaternary)"]')
+
+ expect(elementsWithBlue.length).toBeGreaterThan(0)
+ expect(elementsWithQuaternary.length).toBeGreaterThan(0)
+ })
+ })
+ })
+
+ describe('Component Independence', () => {
+ it('should render components independently without side effects', () => {
+ const { container: container1 } = render()
+ const svg1 = container1.querySelector('svg')
+
+ const { container: container2 } = render()
+ const svg2 = container2.querySelector('svg')
+
+ // Components should not affect each other
+ expect(svg1).toBeInTheDocument()
+ expect(svg2).toBeInTheDocument()
+ expect(svg1).not.toBe(svg2)
+ })
+
+ it('should allow selective imports', () => {
+ // Verify that importing only one component works
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toBeInTheDocument()
+ })
+ })
+
+ describe('Bundle Export Pattern', () => {
+ it('should follow barrel export pattern correctly', () => {
+ // All exports should be available from the index
+ expect(Sandbox).toBeDefined()
+ expect(Professional).toBeDefined()
+ expect(Team).toBeDefined()
+ expect(Enterprise).toBeDefined()
+ })
+
+ it('should maintain tree-shaking compatibility', () => {
+ // Each export should be independently usable
+ const components = [Sandbox, Professional, Team, Enterprise]
+
+ components.forEach((Component) => {
+ // Component can be function or object (React.memo wraps it)
+ expect(['function', 'object']).toContain(typeof Component)
+ const { container } = render()
+ expect(container.querySelector('svg')).toBeInTheDocument()
+ })
+ })
+ })
+
+ describe('Real-world Usage Patterns', () => {
+ it('should support rendering in a plan selector', () => {
+ const { container } = render(
+
+
+
+
+
+
,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ const buttons = container.querySelectorAll('button')
+
+ expect(svgs).toHaveLength(4)
+ expect(buttons).toHaveLength(4)
+ })
+
+ it('should support rendering in a comparison table', () => {
+ const { container } = render(
+ ,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(4)
+ })
+
+ it('should support conditional rendering', () => {
+ const renderPlan = (planType: 'sandbox' | 'professional' | 'team' | 'enterprise') => (
+
+ {planType === 'sandbox' &&
}
+ {planType === 'professional' &&
}
+ {planType === 'team' &&
}
+ {planType === 'enterprise' &&
}
+
+ )
+
+ const { container } = render(renderPlan('team'))
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(1)
+ })
+
+ it('should support dynamic rendering from array', () => {
+ const plans = [
+ { id: 'sandbox', Icon: Sandbox },
+ { id: 'professional', Icon: Professional },
+ { id: 'team', Icon: Team },
+ { id: 'enterprise', Icon: Enterprise },
+ ]
+
+ const { container } = render(
+
+ {plans.map(({ id, Icon }) => (
+
+
+
+ ))}
+
,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(4)
+ })
+ })
+
+ describe('Performance', () => {
+ it('should handle rapid re-renders efficiently', () => {
+ const { container, rerender } = render(
+ ,
+ )
+
+ // Simulate multiple re-renders
+ for (let i = 0; i < 5; i++) {
+ rerender(
+
+
+
+
,
+ )
+ }
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(2)
+ })
+
+ it('should handle large lists efficiently', () => {
+ const { container } = render(
+
+ {Array.from({ length: 20 }).map((_, i) => {
+ const components = [Sandbox, Professional, Team, Enterprise]
+ const Component = components[i % 4]
+ return
+ })}
+
,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(20)
+ })
+ })
+})
diff --git a/web/app/components/billing/plan/assets/professional.spec.tsx b/web/app/components/billing/plan/assets/professional.spec.tsx
new file mode 100644
index 0000000000..0fb84e2870
--- /dev/null
+++ b/web/app/components/billing/plan/assets/professional.spec.tsx
@@ -0,0 +1,172 @@
+import { render } from '@testing-library/react'
+import Professional from './professional'
+
+describe('Professional Icon Component', () => {
+ describe('Rendering', () => {
+ it('should render without crashing', () => {
+ const { container } = render()
+ expect(container.firstChild).toBeInTheDocument()
+ })
+
+ it('should render an SVG element', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('should have correct SVG attributes', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toHaveAttribute('xmlns', 'http://www.w3.org/2000/svg')
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ expect(svg).toHaveAttribute('viewBox', '0 0 32 32')
+ expect(svg).toHaveAttribute('fill', 'none')
+ })
+
+ it('should render correct number of SVG rect elements', () => {
+ const { container } = render()
+ const rects = container.querySelectorAll('rect')
+
+ // Based on the component structure, it should have multiple rect elements
+ expect(rects.length).toBeGreaterThan(0)
+ })
+
+ it('should render elements with correct fill colors', () => {
+ const { container } = render()
+ const blueElements = container.querySelectorAll('[fill="var(--color-saas-dify-blue-inverted)"]')
+ const quaternaryElements = container.querySelectorAll('[fill="var(--color-text-quaternary)"]')
+
+ expect(blueElements.length).toBeGreaterThan(0)
+ expect(quaternaryElements.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('Component Behavior', () => {
+ it('should render consistently across multiple renders', () => {
+ const { container: container1 } = render()
+ const { container: container2 } = render()
+
+ expect(container1.innerHTML).toBe(container2.innerHTML)
+ })
+
+ it('should not be wrapped with React.memo', () => {
+ // Professional component is exported directly without React.memo
+ // This test ensures the component renders correctly without memoization
+ const { container, rerender } = render()
+ const firstRender = container.innerHTML
+
+ rerender()
+ const secondRender = container.innerHTML
+
+ // Content should still be the same even without memoization
+ expect(firstRender).toBe(secondRender)
+ })
+ })
+
+ describe('Accessibility', () => {
+ it('should render as a decorative image (no accessibility concerns for icon)', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ // SVG icons typically don't need aria-labels if they're decorative
+ // This test ensures the SVG is present and renderable
+ expect(svg).toBeInTheDocument()
+ })
+ })
+
+ describe('Edge Cases', () => {
+ it('should handle multiple instances without conflicts', () => {
+ const { container } = render(
+ <>
+
+
+
+ >,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(3)
+ })
+
+ it('should maintain structure when wrapped in other elements', () => {
+ const { container } = render(
+ ,
+ )
+
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ expect(svg?.getAttribute('width')).toBe('32')
+ })
+
+ it('should render in different contexts without errors', () => {
+ const { container } = render(
+ ,
+ )
+
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+ })
+
+ describe('CSS Variables', () => {
+ it('should use CSS custom properties for colors', () => {
+ const { container } = render()
+ const elementsWithCSSVars = container.querySelectorAll('[fill*="var("]')
+
+ // All fill attributes should use CSS variables
+ expect(elementsWithCSSVars.length).toBeGreaterThan(0)
+ })
+
+ it('should have opacity attributes on quaternary elements', () => {
+ const { container } = render()
+ const quaternaryElements = container.querySelectorAll('[fill="var(--color-text-quaternary)"]')
+
+ quaternaryElements.forEach((element) => {
+ expect(element).toHaveAttribute('opacity', '0.18')
+ })
+ })
+
+ it('should not have opacity on blue inverted elements', () => {
+ const { container } = render()
+ const blueElements = container.querySelectorAll('[fill="var(--color-saas-dify-blue-inverted)"]')
+
+ blueElements.forEach((element) => {
+ expect(element).not.toHaveAttribute('opacity')
+ })
+ })
+ })
+
+ describe('SVG Structure', () => {
+ it('should have correct rect element structure', () => {
+ const { container } = render()
+ const rects = container.querySelectorAll('rect')
+
+ // Each rect should have specific attributes
+ rects.forEach((rect) => {
+ expect(rect).toHaveAttribute('width', '2')
+ expect(rect).toHaveAttribute('height', '2')
+ expect(rect).toHaveAttribute('rx', '1')
+ expect(rect).toHaveAttribute('fill')
+ })
+ })
+
+ it('should maintain exact pixel positioning', () => {
+ const { container } = render()
+ const rects = container.querySelectorAll('rect')
+
+ // Ensure positioning attributes exist
+ rects.forEach((rect) => {
+ expect(rect).toHaveAttribute('x')
+ expect(rect).toHaveAttribute('y')
+ })
+ })
+ })
+})
diff --git a/web/app/components/billing/plan/assets/sandbox.spec.tsx b/web/app/components/billing/plan/assets/sandbox.spec.tsx
new file mode 100644
index 0000000000..5a5accf362
--- /dev/null
+++ b/web/app/components/billing/plan/assets/sandbox.spec.tsx
@@ -0,0 +1,128 @@
+import { render } from '@testing-library/react'
+import React from 'react'
+import Sandbox from './sandbox'
+
+describe('Sandbox Icon Component', () => {
+ describe('Rendering', () => {
+ it('should render without crashing', () => {
+ const { container } = render()
+ expect(container.firstChild).toBeInTheDocument()
+ })
+
+ it('should render an SVG element', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('should have correct SVG attributes', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toHaveAttribute('xmlns', 'http://www.w3.org/2000/svg')
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ expect(svg).toHaveAttribute('viewBox', '0 0 32 32')
+ expect(svg).toHaveAttribute('fill', 'none')
+ })
+
+ it('should render correct number of SVG elements', () => {
+ const { container } = render()
+ const rects = container.querySelectorAll('rect')
+ const paths = container.querySelectorAll('path')
+
+ // Based on the component structure
+ expect(rects.length).toBeGreaterThan(0)
+ expect(paths.length).toBeGreaterThan(0)
+ })
+
+ it('should render elements with correct fill colors', () => {
+ const { container } = render()
+ const blueElements = container.querySelectorAll('[fill="var(--color-saas-dify-blue-inverted)"]')
+ const quaternaryElements = container.querySelectorAll('[fill="var(--color-text-quaternary)"]')
+
+ expect(blueElements.length).toBeGreaterThan(0)
+ expect(quaternaryElements.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('Component Behavior', () => {
+ it('should be memoized with React.memo', () => {
+ // React.memo wraps the component, so the display name should indicate memoization
+ // The component itself should be stable across re-renders
+ const { rerender, container } = render()
+ const firstRender = container.innerHTML
+
+ rerender()
+ const secondRender = container.innerHTML
+
+ expect(firstRender).toBe(secondRender)
+ })
+
+ it('should render consistently across multiple renders', () => {
+ const { container: container1 } = render()
+ const { container: container2 } = render()
+
+ expect(container1.innerHTML).toBe(container2.innerHTML)
+ })
+ })
+
+ describe('Accessibility', () => {
+ it('should render as a decorative image (no accessibility concerns for icon)', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ // SVG icons typically don't need aria-labels if they're decorative
+ // This test ensures the SVG is present and renderable
+ expect(svg).toBeInTheDocument()
+ })
+ })
+
+ describe('Edge Cases', () => {
+ it('should handle multiple instances without conflicts', () => {
+ const { container } = render(
+ <>
+
+
+
+ >,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(3)
+ })
+
+ it('should maintain structure when wrapped in other elements', () => {
+ const { container } = render(
+
+
+
+
+
,
+ )
+
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ expect(svg?.getAttribute('width')).toBe('32')
+ })
+ })
+
+ describe('CSS Variables', () => {
+ it('should use CSS custom properties for colors', () => {
+ const { container } = render()
+ const elementsWithCSSVars = container.querySelectorAll('[fill*="var("]')
+
+ // All fill attributes should use CSS variables
+ expect(elementsWithCSSVars.length).toBeGreaterThan(0)
+ })
+
+ it('should have opacity attributes on quaternary elements', () => {
+ const { container } = render()
+ const quaternaryElements = container.querySelectorAll('[fill="var(--color-text-quaternary)"]')
+
+ quaternaryElements.forEach((element) => {
+ expect(element).toHaveAttribute('opacity', '0.18')
+ })
+ })
+ })
+})
diff --git a/web/app/components/billing/plan/assets/team.spec.tsx b/web/app/components/billing/plan/assets/team.spec.tsx
new file mode 100644
index 0000000000..60e69aa280
--- /dev/null
+++ b/web/app/components/billing/plan/assets/team.spec.tsx
@@ -0,0 +1,199 @@
+import { render } from '@testing-library/react'
+import Team from './team'
+
+describe('Team Icon Component', () => {
+ describe('Rendering', () => {
+ it('should render without crashing', () => {
+ const { container } = render()
+ expect(container.firstChild).toBeInTheDocument()
+ })
+
+ it('should render an SVG element', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('should have correct SVG attributes', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toHaveAttribute('xmlns', 'http://www.w3.org/2000/svg')
+ expect(svg).toHaveAttribute('width', '32')
+ expect(svg).toHaveAttribute('height', '32')
+ expect(svg).toHaveAttribute('viewBox', '0 0 32 32')
+ expect(svg).toHaveAttribute('fill', 'none')
+ })
+
+ it('should render both rect and path elements', () => {
+ const { container } = render()
+ const rects = container.querySelectorAll('rect')
+ const paths = container.querySelectorAll('path')
+
+ // Team icon uses both rects and paths
+ expect(rects.length).toBeGreaterThan(0)
+ expect(paths.length).toBeGreaterThan(0)
+ })
+
+ it('should render elements with correct fill colors', () => {
+ const { container } = render()
+ const blueElements = container.querySelectorAll('[fill="var(--color-saas-dify-blue-inverted)"]')
+ const quaternaryElements = container.querySelectorAll('[fill="var(--color-text-quaternary)"]')
+
+ expect(blueElements.length).toBeGreaterThan(0)
+ expect(quaternaryElements.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('Component Behavior', () => {
+ it('should render consistently across multiple renders', () => {
+ const { container: container1 } = render()
+ const { container: container2 } = render()
+
+ expect(container1.innerHTML).toBe(container2.innerHTML)
+ })
+
+ it('should maintain stable output without memoization', () => {
+ const { container, rerender } = render()
+ const firstRender = container.innerHTML
+
+ rerender()
+ const secondRender = container.innerHTML
+
+ expect(firstRender).toBe(secondRender)
+ })
+ })
+
+ describe('Accessibility', () => {
+ it('should render as a decorative image', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('should be usable in accessible contexts', () => {
+ const { container } = render(
+
+
+
,
+ )
+
+ const wrapper = container.querySelector('[role="img"]')
+ expect(wrapper).toBeInTheDocument()
+ expect(wrapper).toHaveAttribute('aria-label', 'Team plan')
+ })
+ })
+
+ describe('Edge Cases', () => {
+ it('should handle multiple instances without conflicts', () => {
+ const { container } = render(
+ <>
+
+
+
+ >,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(3)
+ })
+
+ it('should maintain structure when wrapped in other elements', () => {
+ const { container } = render(
+
+
+
+
+
,
+ )
+
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ expect(svg?.getAttribute('width')).toBe('32')
+ })
+
+ it('should render correctly in list context', () => {
+ const { container } = render(
+ ,
+ )
+
+ const svgs = container.querySelectorAll('svg')
+ expect(svgs).toHaveLength(2)
+ })
+ })
+
+ describe('CSS Variables', () => {
+ it('should use CSS custom properties for colors', () => {
+ const { container } = render()
+ const elementsWithCSSVars = container.querySelectorAll('[fill*="var("]')
+
+ expect(elementsWithCSSVars.length).toBeGreaterThan(0)
+ })
+
+ it('should have opacity attributes on quaternary path elements', () => {
+ const { container } = render()
+ const quaternaryPaths = container.querySelectorAll('path[fill="var(--color-text-quaternary)"]')
+
+ quaternaryPaths.forEach((path) => {
+ expect(path).toHaveAttribute('opacity', '0.18')
+ })
+ })
+
+ it('should not have opacity on blue inverted elements', () => {
+ const { container } = render()
+ const blueRects = container.querySelectorAll('rect[fill="var(--color-saas-dify-blue-inverted)"]')
+
+ blueRects.forEach((rect) => {
+ expect(rect).not.toHaveAttribute('opacity')
+ })
+ })
+ })
+
+ describe('SVG Structure', () => {
+ it('should have correct rect element attributes', () => {
+ const { container } = render()
+ const rects = container.querySelectorAll('rect')
+
+ rects.forEach((rect) => {
+ expect(rect).toHaveAttribute('x')
+ expect(rect).toHaveAttribute('y')
+ expect(rect).toHaveAttribute('width', '2')
+ expect(rect).toHaveAttribute('height', '2')
+ expect(rect).toHaveAttribute('rx', '1')
+ expect(rect).toHaveAttribute('fill')
+ })
+ })
+
+ it('should have correct path element structure', () => {
+ const { container } = render()
+ const paths = container.querySelectorAll('path')
+
+ paths.forEach((path) => {
+ expect(path).toHaveAttribute('d')
+ expect(path).toHaveAttribute('fill')
+ })
+ })
+
+ it('should maintain proper element positioning', () => {
+ const { container } = render()
+ const svg = container.querySelector('svg')
+
+ expect(svg?.childNodes.length).toBeGreaterThan(0)
+ })
+ })
+
+ describe('Export', () => {
+ it('should be the default export', () => {
+ expect(Team).toBeDefined()
+ expect(typeof Team).toBe('function')
+ })
+ })
+})