diff --git a/src/views/NewSettings/components/Usage/UsageBar.test.tsx b/src/views/NewSettings/components/Usage/UsageBar.test.tsx new file mode 100644 index 0000000000..2feadb2641 --- /dev/null +++ b/src/views/NewSettings/components/Usage/UsageBar.test.tsx @@ -0,0 +1,95 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, test, vi } from 'vitest'; +import UsageBar from './UsageBar'; + +vi.mock('@internxt/ui', () => ({ + Tooltip: ({ + title, + subtitle, + className, + children, + }: { + title: string; + subtitle: string; + className: string; + children: React.ReactNode; + }) => ( +
+ {`${title}: ${subtitle}`} + {children} +
+ ), +})); + +const GB = 1024 ** 3; + +const renderBar = (props = {}) => + render(); + +const getSegmentElement = (title: string) => screen.getByTestId(`segment-${title}`).parentElement as HTMLElement; + +const getSegmentPercentage = (title: string) => Number.parseFloat(getSegmentElement(title).style.width); + +describe('UsageBar', () => { + test('when usage is over the limit, then both segments are red', () => { + renderBar({ driveUsage: 30 * GB, backupsUsage: 10 * GB }); + + expect(screen.getByTestId('segment-Drive').className).toContain('bg-red'); + expect(screen.getByTestId('segment-Backup').className).toContain('bg-red'); + }); + + test('when usage is over the limit, then the segments together fill exactly 100%', () => { + renderBar({ driveUsage: 30 * GB, backupsUsage: 10 * GB }); + + expect(getSegmentPercentage('Drive') + getSegmentPercentage('Backup')).toBeCloseTo(100); + }); + + test('when usage is under the limit, then the segments keep their regular colors and percentages', () => { + renderBar({ driveUsage: GB / 4, backupsUsage: GB / 4 }); + + expect(screen.getByTestId('segment-Drive').className).toContain('bg-primary'); + expect(screen.getByTestId('segment-Backup').className).toContain('bg-indigo'); + expect(getSegmentPercentage('Drive')).toBe(25); + }); + + test('when drive usage is non-zero, then the drive segment has a 5px minimum width', () => { + renderBar({ driveUsage: 1 }); + + expect(getSegmentElement('Drive').className).toContain('min-w-[5px]'); + }); + + test('when drive usage is zero, then the drive segment has no minimum width', () => { + renderBar(); + + expect(getSegmentElement('Drive').className).not.toContain('min-w-[5px]'); + }); + + test('when drive and backups usage differ, then the backup tooltip shows the backups usage', () => { + renderBar({ driveUsage: 2 * GB, backupsUsage: 3 * GB, spaceLimit: 10 * GB }); + + expect(screen.getByText('Drive: 2GB')).toBeInTheDocument(); + expect(screen.getByText('Backup: 3GB')).toBeInTheDocument(); + }); + + test('when only drive has usage, then the drive segment has no divider margin and is rounded on both sides', () => { + renderBar({ driveUsage: 2 * GB }); + + const { className } = screen.getByTestId('segment-Drive'); + expect(className).not.toContain('mr-0.5'); + expect(className).toContain('rounded-lg'); + }); + + test('when only backups have usage, then the backup segment is rounded on both sides', () => { + renderBar({ backupsUsage: GB / 2 }); + + expect(screen.getByTestId('segment-Backup').className).toContain('rounded-lg'); + }); + + test('when drive and backups have usage, then the divider margin is kept on the drive segment', () => { + renderBar({ driveUsage: GB / 4, backupsUsage: GB / 4 }); + + expect(screen.getByTestId('segment-Drive').className).toContain('mr-0.5'); + expect(screen.getByTestId('segment-Drive').className).toContain('rounded-l-lg'); + expect(screen.getByTestId('segment-Backup').className).toContain('rounded-r-lg'); + }); +}); diff --git a/src/views/NewSettings/components/Usage/UsageBar.tsx b/src/views/NewSettings/components/Usage/UsageBar.tsx index 629908230d..92df8639db 100644 --- a/src/views/NewSettings/components/Usage/UsageBar.tsx +++ b/src/views/NewSettings/components/Usage/UsageBar.tsx @@ -1,6 +1,6 @@ -import { useEffect, useRef, useState } from 'react'; import { bytesToString } from 'app/drive/services/size.service'; import { Tooltip } from '@internxt/ui'; +import { getUsageBarPercentages } from 'views/NewSettings/utils'; const UsageBar = ({ backupsUsage, @@ -13,52 +13,47 @@ const UsageBar = ({ spaceLimit: string | number; height: string | number; }) => { - const [barWidth, setBarWidth] = useState(0); - const barRef = useRef(null); const spaceLimitBytes = Number(spaceLimit); const driveUsageBytes = Number(driveUsage); const backupsUsageBytes = Number(backupsUsage); - const percentageDriveUsed = (driveUsageBytes * 100) / spaceLimitBytes; - const percentageBackupUsed = (backupsUsageBytes * 100) / spaceLimitBytes; - const driveWidth = (barWidth * percentageDriveUsed) / 100; - const backupWidth = (barWidth * percentageBackupUsed) / 100; - - useEffect(() => { - const bar = barRef.current; - - if (bar) { - const listener = () => { - setBarWidth(bar.getBoundingClientRect().width); - }; - bar.addEventListener('resize', listener); - - listener(); - - return () => bar.removeEventListener('resize', listener); - } - }, []); + const { drivePercentage, backupPercentage, isOverLimit } = getUsageBarPercentages({ + driveUsageBytes, + backupsUsageBytes, + spaceLimitBytes, + }); + const driveColor = isOverLimit ? 'bg-red' : 'bg-primary'; + const backupColor = isOverLimit ? 'bg-red' : 'bg-indigo'; + const hasDriveSegment = drivePercentage > 0; + const hasBackupsSegment = backupPercentage > 0; + const driveShapeClass = hasBackupsSegment ? 'mr-0.5 rounded-l-lg' : 'rounded-lg'; + const backupShapeClass = hasDriveSegment ? 'rounded-r-lg' : 'rounded-lg'; + const driveMinWidthClass = drivePercentage > 0 ? 'min-w-[5px]' : ''; return ( -
- -
0 && driveWidth < 5 ? 5 : driveWidth}px` }}>
-
- - -
-
+
+
+ +
+
+
+ +
+ +
+
+
); }; diff --git a/src/views/NewSettings/utils/index.ts b/src/views/NewSettings/utils/index.ts index 74d8e540ca..3ec1b8537c 100644 --- a/src/views/NewSettings/utils/index.ts +++ b/src/views/NewSettings/utils/index.ts @@ -2,3 +2,4 @@ export * from './LogsUtils'; export * from './membersUtils'; export * from './productUtils'; export * from './suscriptionUtils'; +export * from './usageBarUtils'; diff --git a/src/views/NewSettings/utils/usageBarUtils.test.ts b/src/views/NewSettings/utils/usageBarUtils.test.ts new file mode 100644 index 0000000000..6954487ec7 --- /dev/null +++ b/src/views/NewSettings/utils/usageBarUtils.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from 'vitest'; +import { getUsageBarPercentages } from './usageBarUtils'; + +const GB = 1024 ** 3; + +const arrange = (overrides = {}) => + getUsageBarPercentages({ + driveUsageBytes: 0, + backupsUsageBytes: 0, + spaceLimitBytes: 10 * GB, + ...overrides, + }); + +describe('getUsageBarPercentages', () => { + test('when usage is under the limit, then percentages are relative to the limit and not over limit', () => { + const result = arrange({ driveUsageBytes: 2.5 * GB, backupsUsageBytes: 5 * GB }); + + expect(result).toEqual({ drivePercentage: 25, backupPercentage: 50, isOverLimit: false }); + }); + + test('when usage is exactly the limit, then the bar is full and not over limit', () => { + const result = arrange({ driveUsageBytes: 4 * GB, backupsUsageBytes: 6 * GB }); + + expect(result).toEqual({ drivePercentage: 40, backupPercentage: 60, isOverLimit: false }); + }); + + test('when drive usage is far over the limit, then the drive segment takes the whole bar', () => { + const result = arrange({ driveUsageBytes: 33.8 * GB, spaceLimitBytes: GB }); + + expect(result).toEqual({ drivePercentage: 100, backupPercentage: 0, isOverLimit: true }); + }); + + test('when drive plus backups are over the limit, then they are scaled to 100% keeping their ratio', () => { + const result = arrange({ driveUsageBytes: 15 * GB, backupsUsageBytes: 5 * GB }); + + expect(result).toEqual({ drivePercentage: 75, backupPercentage: 25, isOverLimit: true }); + }); + + test('when the limit is zero and there is usage, then the bar is full and over limit', () => { + const result = arrange({ driveUsageBytes: GB, spaceLimitBytes: 0 }); + + expect(result).toEqual({ drivePercentage: 100, backupPercentage: 0, isOverLimit: true }); + }); + + test('when the limit is invalid and there is no usage, then nothing is drawn', () => { + const result = arrange({ spaceLimitBytes: Number.NaN }); + + expect(result).toEqual({ drivePercentage: 0, backupPercentage: 0, isOverLimit: false }); + }); + + test('when values are not finite, then percentages are never NaN or Infinity', () => { + const result = arrange({ driveUsageBytes: Number.NaN, backupsUsageBytes: Infinity, spaceLimitBytes: Infinity }); + + expect(Number.isFinite(result.drivePercentage)).toBe(true); + expect(Number.isFinite(result.backupPercentage)).toBe(true); + }); + + test('when usage is zero, then both segments are empty', () => { + expect(arrange()).toEqual({ drivePercentage: 0, backupPercentage: 0, isOverLimit: false }); + }); +}); diff --git a/src/views/NewSettings/utils/usageBarUtils.ts b/src/views/NewSettings/utils/usageBarUtils.ts new file mode 100644 index 0000000000..a368c0b954 --- /dev/null +++ b/src/views/NewSettings/utils/usageBarUtils.ts @@ -0,0 +1,42 @@ +interface UsageBarPercentagesParams { + driveUsageBytes: number; + backupsUsageBytes: number; + spaceLimitBytes: number; +} + +interface UsageBarPercentages { + drivePercentage: number; + backupPercentage: number; + isOverLimit: boolean; +} + +const toNonNegativeNumber = (value: number): number => (Number.isFinite(value) && value > 0 ? value : 0); + +/** + * Computes the share of the bar (0-100) taken by the Drive and Backups segments. + * Over the limit (or with an invalid limit and some usage) both segments are scaled + * proportionally so together they fill exactly 100%. + */ +export const getUsageBarPercentages = ({ + driveUsageBytes, + backupsUsageBytes, + spaceLimitBytes, +}: UsageBarPercentagesParams): UsageBarPercentages => { + const driveUsage = toNonNegativeNumber(driveUsageBytes); + const backupsUsage = toNonNegativeNumber(backupsUsageBytes); + const spaceLimit = toNonNegativeNumber(spaceLimitBytes); + const totalUsage = driveUsage + backupsUsage; + const isOverLimit = totalUsage > spaceLimit; + + if (totalUsage === 0) { + return { drivePercentage: 0, backupPercentage: 0, isOverLimit }; + } + + const denominator = isOverLimit ? totalUsage : spaceLimit; + + return { + drivePercentage: (driveUsage * 100) / denominator, + backupPercentage: (backupsUsage * 100) / denominator, + isOverLimit, + }; +};