diff --git a/frontend/src/components/HomeComponents/SetupGuide/SetupGuide.test.tsx b/frontend/src/components/HomeComponents/SetupGuide/SetupGuide.test.tsx
index aa1dbf43..bf261e8a 100644
--- a/frontend/src/components/HomeComponents/SetupGuide/SetupGuide.test.tsx
+++ b/frontend/src/components/HomeComponents/SetupGuide/SetupGuide.test.tsx
@@ -10,8 +10,23 @@ jest.mock('@/components/utils/URLs', () => ({
// Mock CopyableCode component
jest.mock('./CopyableCode', () => ({
- CopyableCode: ({ text }: { text: string }) => (
-
{text}
+ CopyableCode: ({
+ text,
+ copyText,
+ isSensitive,
+ }: {
+ text: string;
+ copyText: string;
+ isSensitive?: boolean;
+ }) => (
+
+ {text}
+
),
}));
@@ -21,6 +36,19 @@ jest.mock('./utils', () => ({
exportConfigSetup: (props: any) => mockExportConfigSetup(props),
}));
+// Mock Button component
+jest.mock('@/components/ui/button', () => ({
+ Button: ({ children, variant, onClick }: any) => (
+
+ ),
+}));
+
const defaultProps = {
name: 'Test User',
encryption_secret: 'secret123',
@@ -28,6 +56,10 @@ const defaultProps = {
};
describe('SetupGuide', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
test('renders setup guide sections', () => {
render();
@@ -40,6 +72,13 @@ describe('SetupGuide', () => {
expect(screen.getByText('SYNC')).toBeInTheDocument();
});
+ test('renders main heading with gradient text', () => {
+ render();
+
+ expect(screen.getByText('Setup')).toBeInTheDocument();
+ expect(screen.getByText('Guide')).toBeInTheDocument();
+ });
+
test('renders configuration commands using props', () => {
render();
@@ -58,31 +97,255 @@ describe('SetupGuide', () => {
).toBeInTheDocument();
});
+ test('renders all CopyableCode components with correct props', () => {
+ render();
+
+ const copyableCodes = screen.getAllByTestId('copyable-code');
+ expect(copyableCodes.length).toBe(5);
+
+ // Check prerequisites CopyableCode
+ expect(copyableCodes[0]).toHaveAttribute('data-text', 'task --version');
+ expect(copyableCodes[0]).toHaveAttribute('data-copytext', 'task --version');
+ expect(copyableCodes[0]).toHaveAttribute('data-issensitive', 'false');
+
+ // Check encryption secret CopyableCode (sensitive)
+ expect(copyableCodes[1]).toHaveAttribute(
+ 'data-text',
+ `task config sync.encryption_secret ${defaultProps.encryption_secret}`
+ );
+ expect(copyableCodes[1]).toHaveAttribute('data-issensitive', 'true');
+
+ // Check origin CopyableCode
+ expect(copyableCodes[2]).toHaveAttribute(
+ 'data-text',
+ 'task config sync.server.origin https://test-container'
+ );
+ expect(copyableCodes[2]).toHaveAttribute('data-issensitive', 'false');
+
+ // Check client ID CopyableCode (sensitive)
+ expect(copyableCodes[3]).toHaveAttribute(
+ 'data-text',
+ `task config sync.server.client_id ${defaultProps.uuid}`
+ );
+ expect(copyableCodes[3]).toHaveAttribute('data-issensitive', 'true');
+
+ // Check sync init CopyableCode
+ expect(copyableCodes[4]).toHaveAttribute('data-text', 'task sync init');
+ expect(copyableCodes[4]).toHaveAttribute('data-issensitive', 'false');
+ });
+
+ test('renders instructional text content', () => {
+ render();
+
+ expect(
+ screen.getByText(/Ensure that Taskwarrior 3.0 or greater is installed/)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/You will need an encryption secret/)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/Configure Taskwarrior with these commands/)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/For more information about how this works/)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/Finally, setup the sync for your Taskwarrior client/)
+ ).toBeInTheDocument();
+ });
+
test('clicking download configuration triggers download logic', () => {
- mockExportConfigSetup.mockReturnValue('config-content');
+ const configContent = 'test-config-content';
+ mockExportConfigSetup.mockReturnValue(configContent);
// Polyfill missing browser APIs
+ const createObjectURLSpy = jest.fn(() => 'blob:http://localhost/file');
+ const revokeObjectURLSpy = jest.fn();
+
Object.defineProperty(global.URL, 'createObjectURL', {
writable: true,
- value: jest.fn(() => 'blob:http://localhost/file'),
+ value: createObjectURLSpy,
});
Object.defineProperty(global.URL, 'revokeObjectURL', {
writable: true,
- value: jest.fn(),
+ value: revokeObjectURLSpy,
});
- const appendSpy = jest.spyOn(document.body, 'appendChild');
- const removeSpy = jest.spyOn(document.body, 'removeChild');
+ const originalAppendChild = document.body.appendChild.bind(document.body);
+ const originalRemoveChild = document.body.removeChild.bind(document.body);
+
+ const appendSpy = jest
+ .spyOn(document.body, 'appendChild')
+ .mockImplementation((node) => {
+ // Actually append to DOM so removeChild works
+ return originalAppendChild(node);
+ });
+
+ const removeSpy = jest
+ .spyOn(document.body, 'removeChild')
+ .mockImplementation((node) => {
+ // Only remove if it's actually a child
+ if (node.parentNode === document.body) {
+ return originalRemoveChild(node);
+ }
+ return node;
+ });
render();
- fireEvent.click(screen.getByText(/DOWNLOAD CONFIGURATION/i));
+ // Click on the h3 element which has the onClick handler
+ const downloadHeading = screen
+ .getByText(/DOWNLOAD CONFIGURATION/i)
+ .closest('h3');
+ expect(downloadHeading).toBeInTheDocument();
+
+ if (downloadHeading) {
+ fireEvent.click(downloadHeading);
+ }
expect(mockExportConfigSetup).toHaveBeenCalledWith(defaultProps);
- expect(URL.createObjectURL).toHaveBeenCalled();
- expect(URL.revokeObjectURL).toHaveBeenCalled();
+ expect(createObjectURLSpy).toHaveBeenCalled();
+ expect(revokeObjectURLSpy).toHaveBeenCalled();
expect(appendSpy).toHaveBeenCalled();
expect(removeSpy).toHaveBeenCalled();
+
+ // Verify blob was created with correct content
+ expect(createObjectURLSpy.mock.calls.length).toBeGreaterThan(0);
+ const firstCall = createObjectURLSpy.mock.calls[0] as unknown[];
+ if (firstCall && firstCall.length > 0) {
+ const blobCall = firstCall[0] as unknown;
+ expect(blobCall).toBeInstanceOf(Blob);
+ if (blobCall instanceof Blob) {
+ expect(blobCall.type).toBe('text/plain;charset=utf-8');
+ }
+ }
+
+ // Verify link was configured correctly
+ const linkElement = appendSpy.mock.calls.find(
+ (call) => call[0] instanceof HTMLAnchorElement
+ )?.[0] as HTMLAnchorElement;
+ if (linkElement) {
+ expect(linkElement.download).toBe('taskwarrior-setup.txt');
+ }
+
+ appendSpy.mockRestore();
+ removeSpy.mockRestore();
+ });
+
+ test('download configuration creates blob with correct type', () => {
+ const configContent = 'config-content';
+ mockExportConfigSetup.mockReturnValue(configContent);
+
+ const createObjectURLSpy = jest.fn(() => 'blob:http://localhost/file');
+ Object.defineProperty(global.URL, 'createObjectURL', {
+ writable: true,
+ value: createObjectURLSpy,
+ });
+
+ Object.defineProperty(global.URL, 'revokeObjectURL', {
+ writable: true,
+ value: jest.fn(),
+ });
+
+ const originalAppendChild = document.body.appendChild.bind(document.body);
+ const originalRemoveChild = document.body.removeChild.bind(document.body);
+
+ const appendSpy = jest
+ .spyOn(document.body, 'appendChild')
+ .mockImplementation((node) => {
+ return originalAppendChild(node);
+ });
+
+ const removeSpy = jest
+ .spyOn(document.body, 'removeChild')
+ .mockImplementation((node) => {
+ if (node.parentNode === document.body) {
+ return originalRemoveChild(node);
+ }
+ return node;
+ });
+
+ render();
+
+ const downloadHeading = screen
+ .getByText(/DOWNLOAD CONFIGURATION/i)
+ .closest('h3');
+ if (downloadHeading) {
+ fireEvent.click(downloadHeading);
+ }
+
+ expect(createObjectURLSpy.mock.calls.length).toBeGreaterThan(0);
+ const firstCall = createObjectURLSpy.mock.calls[0] as unknown[];
+ if (firstCall && firstCall.length > 0) {
+ const blob = firstCall[0] as unknown;
+ if (blob instanceof Blob) {
+ expect(blob.type).toBe('text/plain;charset=utf-8');
+ }
+ }
+
+ appendSpy.mockRestore();
+ removeSpy.mockRestore();
+ });
+
+ test('download configuration sets correct filename', () => {
+ mockExportConfigSetup.mockReturnValue('config-content');
+
+ Object.defineProperty(global.URL, 'createObjectURL', {
+ writable: true,
+ value: jest.fn(() => 'blob:http://localhost/file'),
+ });
+
+ Object.defineProperty(global.URL, 'revokeObjectURL', {
+ writable: true,
+ value: jest.fn(),
+ });
+
+ const originalAppendChild = document.body.appendChild.bind(document.body);
+ const originalRemoveChild = document.body.removeChild.bind(document.body);
+
+ const appendSpy = jest
+ .spyOn(document.body, 'appendChild')
+ .mockImplementation((node) => {
+ return originalAppendChild(node);
+ });
+
+ const removeSpy = jest
+ .spyOn(document.body, 'removeChild')
+ .mockImplementation((node) => {
+ if (node.parentNode === document.body) {
+ return originalRemoveChild(node);
+ }
+ return node;
+ });
+
+ render();
+
+ const downloadHeading = screen
+ .getByText(/DOWNLOAD CONFIGURATION/i)
+ .closest('h3');
+ if (downloadHeading) {
+ fireEvent.click(downloadHeading);
+ }
+
+ const linkElement = appendSpy.mock.calls.find(
+ (call) => call[0] instanceof HTMLAnchorElement
+ )?.[0] as HTMLAnchorElement;
+
+ if (linkElement) {
+ expect(linkElement.download).toBe('taskwarrior-setup.txt');
+ }
+
+ appendSpy.mockRestore();
+ removeSpy.mockRestore();
+ });
+
+ test('renders download configuration button', () => {
+ render();
+
+ const downloadText = screen.getByText(/DOWNLOAD CONFIGURATION/i);
+ expect(downloadText).toBeInTheDocument();
+ expect(downloadText.closest('button')).toBeInTheDocument();
+ expect(downloadText.closest('h3')).toBeInTheDocument();
});
});
diff --git a/frontend/src/components/HomeComponents/SetupGuide/__tests__/CopyableCode.test.tsx b/frontend/src/components/HomeComponents/SetupGuide/__tests__/CopyableCode.test.tsx
index b96a6175..8ecc7e5c 100644
--- a/frontend/src/components/HomeComponents/SetupGuide/__tests__/CopyableCode.test.tsx
+++ b/frontend/src/components/HomeComponents/SetupGuide/__tests__/CopyableCode.test.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { CopyableCode } from '../CopyableCode';
import { toast } from 'react-toastify';
@@ -16,10 +17,66 @@ jest.mock('lucide-react', () => ({
EyeOff: () => ,
}));
+// Mock Tooltip component
+jest.mock('@/components/ui/tooltip', () => {
+ return {
+ __esModule: true,
+ default: ({
+ children,
+ title,
+ }: {
+ children: React.ReactNode;
+ title: string;
+ }) => (
+
+ {children}
+
+ ),
+ };
+});
+
+// Mock react-copy-to-clipboard
+jest.mock('react-copy-to-clipboard', () => {
+ return {
+ __esModule: true,
+ default: ({
+ children,
+ onCopy,
+ }: {
+ children: React.ReactNode;
+ text: string;
+ onCopy?: () => void;
+ }) => {
+ // Clone children and add onClick handler that calls onCopy
+ const childrenWithClick = React.Children.map(children, (child) => {
+ if (React.isValidElement(child)) {
+ const originalOnClick = (child as any).props?.onClick;
+ return React.cloneElement(child as React.ReactElement, {
+ onClick: (e: React.MouseEvent) => {
+ if (originalOnClick) {
+ originalOnClick(e);
+ }
+ if (onCopy) {
+ onCopy();
+ }
+ },
+ });
+ }
+ return child;
+ });
+ return <>{childrenWithClick}>;
+ },
+ };
+});
+
const sampleText = 'Sample code';
const sampleCopyText = 'Copy this text';
describe('CopyableCode', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('renders correctly with given text', () => {
render();
@@ -30,7 +87,13 @@ describe('CopyableCode', () => {
it('copies text to clipboard and shows toast message', async () => {
render();
- fireEvent.click(screen.getByTestId('copy-icon'));
+ const copyButton = screen.getByTestId('copy-icon').closest('button');
+ expect(copyButton).toBeInTheDocument();
+
+ if (copyButton) {
+ // Click the button which is wrapped by CopyToClipboard
+ fireEvent.click(copyButton);
+ }
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith(
@@ -47,13 +110,216 @@ describe('CopyableCode', () => {
);
});
});
-});
-describe('SetupGuide component using snapshot', () => {
- test('renders correctly', () => {
- const { asFragment } = render(
-
+ it('does not render sensitive toggle button when isSensitive is false', () => {
+ render(
+
+ );
+
+ expect(screen.queryByTestId('eye-icon')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('eye-off-icon')).not.toBeInTheDocument();
+ });
+
+ it('renders sensitive toggle button when isSensitive is true', () => {
+ render(
+
+ );
+
+ expect(screen.getByTestId('eye-off-icon')).toBeInTheDocument();
+ });
+
+ it('shows full text when isSensitive is true and showSensitive is true', () => {
+ render(
+
+ );
+
+ expect(screen.getByText(sampleText)).toBeInTheDocument();
+ });
+
+ it('masks sensitive value when isSensitive is true and showSensitive is false', () => {
+ const sensitiveText = 'task config sync.encryption_secret my-secret-key';
+ render(
+
+ );
+
+ const toggleButton = screen.getByTestId('eye-off-icon').closest('button');
+ expect(toggleButton).toBeInTheDocument();
+
+ if (toggleButton) {
+ fireEvent.click(toggleButton);
+ }
+
+ const maskedValue = '•'.repeat('my-secret-key'.length);
+ expect(
+ screen.getByText(`task config sync.encryption_secret ${maskedValue}`)
+ ).toBeInTheDocument();
+ });
+
+ it('toggles between showing and hiding sensitive value', () => {
+ const sensitiveText = 'task config sync.encryption_secret secret123';
+ render(
+
+ );
+
+ expect(screen.getByText(sensitiveText)).toBeInTheDocument();
+ expect(screen.getByTestId('eye-off-icon')).toBeInTheDocument();
+
+ const toggleButton = screen.getByTestId('eye-off-icon').closest('button');
+ expect(toggleButton).toBeInTheDocument();
+
+ if (toggleButton) {
+ fireEvent.click(toggleButton);
+ }
+
+ const maskedValue = '•'.repeat('secret123'.length);
+ expect(
+ screen.getByText(`task config sync.encryption_secret ${maskedValue}`)
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('eye-icon')).toBeInTheDocument();
+
+ const showButton = screen.getByTestId('eye-icon').closest('button');
+ if (showButton) {
+ fireEvent.click(showButton);
+ }
+
+ expect(screen.getByText(sensitiveText)).toBeInTheDocument();
+ expect(screen.getByTestId('eye-off-icon')).toBeInTheDocument();
+ });
+
+ it('masks only the last part of text with multiple words', () => {
+ const multiWordText = 'command part1 part2 secret-value';
+ render(
+
+ );
+
+ const toggleButton = screen.getByTestId('eye-off-icon').closest('button');
+ if (toggleButton) {
+ fireEvent.click(toggleButton);
+ }
+
+ const maskedValue = '•'.repeat('secret-value'.length);
+ expect(
+ screen.getByText(`command part1 part2 ${maskedValue}`)
+ ).toBeInTheDocument();
+ });
+
+ it('handles single word text correctly when masking', () => {
+ const singleWord = 'secret-key';
+ render(
+
+ );
+
+ const toggleButton = screen.getByTestId('eye-off-icon').closest('button');
+ if (toggleButton) {
+ fireEvent.click(toggleButton);
+ }
+
+ const maskedValue = '•'.repeat(singleWord.length);
+ expect(screen.getByText(maskedValue)).toBeInTheDocument();
+ });
+
+ it('handles empty string text correctly', () => {
+ const { container } = render(
+
+ );
+
+ const toggleButton = screen.getByTestId('eye-off-icon').closest('button');
+ if (toggleButton) {
+ fireEvent.click(toggleButton);
+ }
+
+ // Check that the code element exists and is empty
+ const codeElement = container.querySelector('code');
+ expect(codeElement).toBeInTheDocument();
+ expect(codeElement?.textContent).toBe('');
+ });
+
+ it('has correct aria-label for toggle button when showing sensitive value', () => {
+ render(
+
+ );
+
+ const toggleButton = screen.getByTestId('eye-off-icon').closest('button');
+ expect(toggleButton).toHaveAttribute('aria-label', 'Hide sensitive value');
+ });
+
+ it('has correct aria-label for toggle button when hiding sensitive value', () => {
+ const sensitiveText = 'task config sync.encryption_secret secret123';
+ render(
+
+ );
+
+ const toggleButton = screen.getByTestId('eye-off-icon').closest('button');
+ if (toggleButton) {
+ fireEvent.click(toggleButton);
+ }
+
+ const showButton = screen.getByTestId('eye-icon').closest('button');
+ expect(showButton).toHaveAttribute('aria-label', 'Show sensitive value');
+ });
+
+ it('has correct tooltip title for toggle button', () => {
+ render(
+
+ );
+
+ const tooltips = screen.getAllByTestId('tooltip');
+ const toggleTooltip = tooltips.find((tooltip) =>
+ tooltip.getAttribute('data-title')?.includes('sensitive')
+ );
+ expect(toggleTooltip).toBeInTheDocument();
+ expect(toggleTooltip?.getAttribute('data-title')).toBe(
+ 'Hide sensitive value'
+ );
+ });
+
+ it('has correct tooltip title for copy button', () => {
+ render();
+
+ const tooltips = screen.getAllByTestId('tooltip');
+ const copyTooltip = tooltips.find(
+ (tooltip) => tooltip.getAttribute('data-title') === 'Copy to clipboard'
);
- expect(asFragment()).toMatchSnapshot();
+ expect(copyTooltip).toBeInTheDocument();
});
});
diff --git a/frontend/src/components/HomeComponents/SetupGuide/__tests__/utils.test.ts b/frontend/src/components/HomeComponents/SetupGuide/__tests__/utils.test.ts
index f59241eb..8dac9855 100644
--- a/frontend/src/components/HomeComponents/SetupGuide/__tests__/utils.test.ts
+++ b/frontend/src/components/HomeComponents/SetupGuide/__tests__/utils.test.ts
@@ -1,39 +1,53 @@
-import { url } from '@/components/utils/URLs';
import { exportConfigSetup } from '../utils';
+import { Props } from '@/components/utils/types';
+import { url } from '@/components/utils/URLs';
describe('exportConfigSetup', () => {
- test('generate correct configuration setup', () => {
- const props = {
- encryption_secret: 'encryptionSecret',
- uuid: 'clients-uuid',
- } as any;
+ const mockProps: Props = {
+ name: 'Test User',
+ encryption_secret: 'test-secret-123',
+ uuid: 'test-uuid-456',
+ };
- const result = exportConfigSetup(props);
+ it('should generate configuration content with all required fields', () => {
+ const result = exportConfigSetup(mockProps);
+ expect(result).toContain('Configure Taskwarrior with these commands');
expect(result).toContain(
- 'Configure Taskwarrior with these commands, run these commands one block at a time'
+ `task config sync.encryption_secret ${mockProps.encryption_secret}`
);
-
expect(result).toContain(
- `task config sync.encryption_secret ${props.encryption_secret}`
+ `task config sync.server.origin ${url.containerOrigin}`
);
-
expect(result).toContain(
- `task config sync.server.origin ${url.containerOrigin}`
+ `task config sync.server.client_id ${mockProps.uuid}`
);
+ expect(result).toContain('task-sync(5) manpage');
+ });
+
+ it('should join all lines with newline characters', () => {
+ const result = exportConfigSetup(mockProps);
+ const lines = result.split('\n');
- expect(result).toContain(`task config sync.server.client_id ${props.uuid}`);
+ expect(lines.length).toBeGreaterThan(1);
+ expect(result).toMatch(/\n/);
});
- test('returns string seprated with newline', () => {
- const props = {
- encryption_secret: 'encryptionSecret',
- uuid: 'clients-uuid',
- } as any;
+ it('should include encryption secret in the output', () => {
+ const result = exportConfigSetup(mockProps);
- const result = exportConfigSetup(props);
+ expect(result).toContain(mockProps.encryption_secret);
+ });
- const lines = result.split('\n');
- expect(lines.length).toBeGreaterThan(1);
+ it('should include UUID in the output', () => {
+ const result = exportConfigSetup(mockProps);
+
+ expect(result).toContain(mockProps.uuid);
+ });
+
+ it('should include container origin URL in the output', () => {
+ const result = exportConfigSetup(mockProps);
+
+ expect(result).toContain(url.containerOrigin);
});
});