Skip to content

Commit a4ad55e

Browse files
feat(CopyToClipboardWidget): Add CopyToClipboard widget with comprehensive tests and styles
- Introduced the CopyToClipboard widget in the widgets module, including options for show_root_heading. - Updated widget configuration to include CopyToClipboardWidget. - Added CSS styles for the CopyToClipboard widget to enhance its display and usability. - Created comprehensive tests for both Python and JavaScript components, covering various scenarios and edge cases. - Expanded documentation to include testing guidelines and integration instructions for the new widget.
1 parent 433dcc1 commit a4ad55e

11 files changed

Lines changed: 1251 additions & 19 deletions

File tree

CONTRIBUTING.md

Lines changed: 208 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,9 @@ print(f'Widget created: {widget.val}')
280280

281281
### Widget Testing
282282

283-
#### Python Tests
283+
**IMPORTANT**: Every new widget must include comprehensive tests for both Python and JavaScript components.
284+
285+
#### Python Tests (Required)
284286

285287
Create `python/tests/test_example.py`:
286288

@@ -289,46 +291,233 @@ import pytest
289291
from numerous.widgets import Example
290292

291293
def test_example_creation():
294+
"""Test basic widget creation with default values."""
292295
widget = Example(value="test", label="Test Label")
293296
assert widget.value == "test"
294297
assert widget.label == "Test Label"
295298
assert widget.disabled is False
296299

300+
def test_example_with_custom_options():
301+
"""Test widget creation with all custom options."""
302+
widget = Example(
303+
value="custom value",
304+
label="Custom Label",
305+
disabled=True,
306+
class_name="custom-class"
307+
)
308+
assert widget.value == "custom value"
309+
assert widget.label == "Custom Label"
310+
assert widget.disabled is True
311+
assert widget.class_name == "custom-class"
312+
297313
def test_example_val_property():
314+
"""Test the val property getter and setter."""
298315
widget = Example(value="initial")
299316
assert widget.val == "initial"
300317

301318
widget.val = "updated"
302319
assert widget.value == "updated"
320+
assert widget.val == "updated"
321+
322+
def test_example_callback():
323+
"""Test callback functionality if widget has callbacks."""
324+
callback_calls = []
325+
326+
def on_change_callback(change):
327+
callback_calls.append(change)
328+
329+
widget = Example(
330+
value="test",
331+
on_change=on_change_callback
332+
)
333+
334+
# Clear initial calls (initialization triggers observers)
335+
callback_calls.clear()
336+
337+
# Simulate state change
338+
widget.some_state = "new_value"
339+
340+
assert len(callback_calls) == 1
341+
assert callback_calls[0]["new"] == "new_value"
342+
343+
def test_example_edge_cases():
344+
"""Test edge cases like empty values, unicode, etc."""
345+
# Empty value
346+
widget = Example(value="")
347+
assert widget.value == ""
348+
349+
# Unicode characters
350+
unicode_text = "Hello 🌍! Unicode: αβγ"
351+
widget = Example(value=unicode_text)
352+
assert widget.value == unicode_text
303353
```
304354

305-
#### JavaScript Tests
355+
#### JavaScript Tests (Required)
306356

307-
Create `js/src/components/widgets/__tests__/ExampleWidget.test.tsx`:
357+
**Create Widget Component Test**: `js/src/components/widgets/__tests__/ExampleWidget.test.tsx`:
308358

309359
```typescript
310360
import React from 'react';
311-
import { render, screen } from '@testing-library/react';
361+
import ExampleWidget from '../ExampleWidget';
312362
import { Example } from '../../ui/Example';
363+
import * as ExampleWidgetModule from '../ExampleWidget';
313364

314-
describe('Example Component', () => {
315-
test('renders with correct props', () => {
316-
const mockOnChange = jest.fn();
317-
render(
318-
<Example
319-
value="test value"
320-
label="Test Label"
321-
disabled={false}
322-
onChange={mockOnChange}
323-
/>
324-
);
325-
326-
expect(screen.getByDisplayValue('test value')).toBeInTheDocument();
327-
expect(screen.getByText('Test Label')).toBeInTheDocument();
328-
});
365+
// Mock dependencies
366+
jest.mock('../../ui/Example', () => ({
367+
Example: jest.fn(() => null)
368+
}));
369+
370+
jest.mock('../../css/styles.scss', () => ({}));
371+
372+
jest.mock('@anywidget/react', () => ({
373+
createRender: jest.fn(comp => comp),
374+
useModelState: jest.fn()
375+
}));
376+
377+
const mockSetValue = jest.fn();
378+
379+
describe('ExampleWidget', () => {
380+
beforeEach(() => {
381+
jest.clearAllMocks();
382+
383+
(jest.requireMock('@anywidget/react').useModelState)
384+
.mockImplementation((key: string) => {
385+
switch (key) {
386+
case 'value': return ['test value', mockSetValue];
387+
case 'label': return ['Test Label'];
388+
case 'disabled': return [false];
389+
default: return [undefined, jest.fn()];
390+
}
391+
});
392+
});
393+
394+
it('renders Example component with correct props', () => {
395+
const useCallbackSpy = jest.spyOn(React, 'useCallback');
396+
useCallbackSpy.mockImplementation(fn => fn);
397+
398+
const ExampleWidgetFunction = (ExampleWidgetModule as any).default.render;
399+
const exampleWidget = ExampleWidgetFunction();
400+
401+
expect(exampleWidget.type).toBe(Example);
402+
expect(exampleWidget.props.value).toBe('test value');
403+
expect(exampleWidget.props.label).toBe('Test Label');
404+
expect(exampleWidget.props.disabled).toBe(false);
405+
expect(typeof exampleWidget.props.onChange).toBe('function');
406+
407+
useCallbackSpy.mockRestore();
408+
});
409+
410+
it('handles value changes correctly', () => {
411+
const useCallbackSpy = jest.spyOn(React, 'useCallback');
412+
useCallbackSpy.mockImplementation(fn => fn);
413+
414+
const ExampleWidgetFunction = (ExampleWidgetModule as any).default.render;
415+
const exampleWidget = ExampleWidgetFunction();
416+
417+
exampleWidget.props.onChange('new value');
418+
419+
expect(mockSetValue).toHaveBeenCalledWith('new value');
420+
421+
useCallbackSpy.mockRestore();
422+
});
329423
});
330424
```
331425

426+
**Create UI Component Test**: `js/src/components/ui/__tests__/Example.test.tsx`:
427+
428+
```typescript
429+
import React from 'react';
430+
import { render, screen, fireEvent } from '@testing-library/react';
431+
import { Example } from '../Example';
432+
433+
describe('Example UI Component', () => {
434+
const defaultProps = {
435+
value: 'test value',
436+
label: 'Test Label',
437+
disabled: false,
438+
onChange: jest.fn(),
439+
};
440+
441+
beforeEach(() => {
442+
jest.clearAllMocks();
443+
});
444+
445+
it('renders correctly with default props', () => {
446+
render(<Example {...defaultProps} />);
447+
448+
expect(screen.getByText('Test Label')).toBeInTheDocument();
449+
expect(screen.getByDisplayValue('test value')).toBeInTheDocument();
450+
});
451+
452+
it('handles user interactions', () => {
453+
render(<Example {...defaultProps} />);
454+
455+
const input = screen.getByDisplayValue('test value');
456+
fireEvent.change(input, { target: { value: 'new value' } });
457+
458+
expect(defaultProps.onChange).toHaveBeenCalledWith('new value');
459+
});
460+
461+
it('respects disabled state', () => {
462+
render(<Example {...defaultProps} disabled={true} />);
463+
464+
const input = screen.getByDisplayValue('test value');
465+
expect(input).toBeDisabled();
466+
});
467+
468+
it('applies custom className', () => {
469+
render(<Example {...defaultProps} className="custom-class" />);
470+
471+
const container = screen.getByText('Test Label').parentElement;
472+
expect(container).toHaveClass('custom-class');
473+
});
474+
});
475+
```
476+
477+
#### Testing Guidelines
478+
479+
1. **Python Test Coverage**:
480+
- Basic widget creation with default values
481+
- Widget creation with all custom options
482+
- Property getters and setters (especially `val` property)
483+
- Callback functionality (if applicable)
484+
- Edge cases: empty values, unicode, special characters
485+
- Error handling scenarios
486+
487+
2. **JavaScript Test Coverage**:
488+
- Widget component renders UI component correctly
489+
- All props are passed through correctly
490+
- State changes trigger correct Python model updates
491+
- User interactions work as expected
492+
- UI component respects all props (disabled, className, etc.)
493+
- Visual states and feedback work correctly
494+
495+
3. **Integration Tests**:
496+
- Widget can be imported and instantiated
497+
- Widget displays in development environment
498+
- Callbacks trigger correctly when user interacts
499+
- State synchronization between Python and JavaScript
500+
501+
#### Running Tests
502+
503+
```bash
504+
# Run Python tests for specific widget
505+
pytest python/tests/test_example.py -v
506+
507+
# Run JavaScript tests for specific widget
508+
cd js
509+
npm test -- Example
510+
511+
# Run all tests
512+
pre-commit run --all-files
513+
```
514+
515+
#### Test File Naming Convention
516+
517+
- **Python**: `python/tests/test_widget_name.py`
518+
- **Widget Component**: `js/src/components/widgets/__tests__/WidgetNameWidget.test.tsx`
519+
- **UI Component**: `js/src/components/ui/__tests__/ComponentName.test.tsx`
520+
332521
## 🧪 Testing
333522

334523
### Running Tests

docs/widgets.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ The widgets are available in the `widgets` module.
1010
options:
1111
show_root_heading: true
1212

13+
## ::: widgets.CopyToClipboard
14+
options:
15+
show_root_heading: true
16+
1317
## ::: widgets.DropDown
1418
options:
1519
show_root_heading: true

0 commit comments

Comments
 (0)