@@ -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
285287Create ` python/tests/test_example.py ` :
286288
@@ -289,46 +291,233 @@ import pytest
289291from numerous.widgets import Example
290292
291293def 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+
297313def 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
310360import React from ' react' ;
311- import { render , screen } from ' @testing-library/react ' ;
361+ import ExampleWidget from ' ../ExampleWidget ' ;
312362import { 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
0 commit comments