Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tasty-apples-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@logto/console": patch
---

refactor(console): use MultiOptionInput for DomainsInput
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
.input {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 _.unit(2) 0 _.unit(3);
justify-content: flex-start;
flex-wrap: wrap;
gap: _.unit(2);
padding: _.unit(1.5) _.unit(3);
background: var(--color-layer-1);
border: 1px solid var(--color-border);
border-radius: 8px;
Expand All @@ -13,59 +15,9 @@
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
font: var(--font-body-2);
cursor: pointer;
cursor: text;
position: relative;

&.multiple {
justify-content: flex-start;
flex-wrap: wrap;
gap: _.unit(2);
padding: _.unit(1.5) _.unit(3);
cursor: text;

.tag {
cursor: auto;
display: flex;
align-items: center;
gap: _.unit(1);
position: relative;

&.focused::after {
content: '';
position: absolute;
inset: 0;
background: var(--color-overlay-default-focused);
}

&.info {
background: var(--color-error-container);
}
}

.close {
width: 16px;
height: 16px;
}

.delete {
width: 20px;
height: 20px;
margin-inline-end: _.unit(-0.5);
}

input {
color: var(--color-text);
font: var(--font-body-2);
background: transparent;
flex-grow: 1;
padding: _.unit(0.5);

&::placeholder {
color: var(--color-placeholder);
}
}
}

&:focus-within {
border-color: var(--color-primary);
outline-color: var(--color-focused-variant);
Expand All @@ -80,6 +32,25 @@
}
}

.tag {
cursor: auto;
display: flex;
align-items: center;
gap: _.unit(1);
position: relative;

&.focused::after {
content: '';
position: absolute;
inset: 0;
background: var(--color-overlay-default-focused);
}
}

.info {
background: var(--color-error-container);
}

.errorMessage {
font: var(--font-body-2);
color: var(--color-error);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';

import DomainsInput from './index';

// Mock styles
jest.mock('./index.module.scss', () => ({
input: 'mock-input-class',
tag: 'mock-tag-class',
info: 'mock-info-class',
}));

// Mock dependencies
jest.mock('react-hook-form', () => ({
useFormContext: jest.fn(),
}));

// Mock MultiOptionInput
jest.mock('@/components/MultiOptionInput', () => ({
__esModule: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: ({ validateInput, onChange, values, placeholder }: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [value, setValue] = useState('');
return (
<div data-testid="multi-option-input">
<div data-testid="values-count">{values.length}</div>
<div data-testid="placeholder">{placeholder}</div>
<input
data-testid="input"
value={value}
onChange={(event) => {
setValue(event.target.value);
}}
/>
<button
data-testid="add-btn"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
const result = validateInput(value);
if (result && typeof result !== 'string' && result.value) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
onChange([...values, result.value]);
}
}}
>
Add
</button>
</div>
);
},
}));

// Mock utils to avoid schema dependencies
jest.mock('./utils', () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
domainOptionsParser: (values: any[]) => ({ values, errorMessage: undefined }),
}));

// Mock shared dependencies
jest.mock(
'@logto/shared/universal',
() => ({
generateStandardShortId: () => 'mock-id',
}),
{ virtual: true }
);

jest.mock(
'@silverhand/essentials',
() => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any, unicorn/prefer-logical-operator-over-ternary
conditional: (condition: any) => (condition ? condition : undefined),
isKeyInObject: () => true,
}),
{ virtual: true }
);

// Mock useTranslation
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => (key === 'placeholder_key' ? 'Enter domain' : key),
}),
}));

const mockSetError = jest.fn();
const mockClearErrors = jest.fn();

describe('DomainsInput', () => {
const defaultProps = {
values: [],
onChange: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
(useFormContext as jest.Mock).mockReturnValue({
setError: mockSetError,
clearErrors: mockClearErrors,
});
});

it('renders correctly', () => {
render(<DomainsInput {...defaultProps} placeholder="placeholder_key" />);
// Check for existence
expect(screen.getByTestId('multi-option-input')).toBeTruthy();
});

it('passes values correctly', () => {
const values = [{ id: '1', value: 'example.com' }];
render(<DomainsInput {...defaultProps} values={values} />);
// Check text content
expect(screen.getByTestId('values-count').textContent).toBe('1');
});

it('handles validation and adding via mocked interaction', () => {
const onChange = jest.fn();
render(<DomainsInput {...defaultProps} onChange={onChange} />);

fireEvent.change(screen.getByTestId('input'), { target: { value: 'test.com' } });
screen.getByTestId('add-btn').click();

expect(onChange).toHaveBeenCalledWith(
expect.arrayContaining([expect.objectContaining({ value: 'test.com' })])
);
});

it('blocks invalid domains', () => {
const onChange = jest.fn();
render(<DomainsInput {...defaultProps} onChange={onChange} />);

fireEvent.change(screen.getByTestId('input'), { target: { value: 'invalid' } });
screen.getByTestId('add-btn').click();

expect(onChange).not.toHaveBeenCalled();
});
});
Loading
Loading