Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6db1ddc
Remove duplicate test file
ajayi-joseph Jan 2, 2026
bd94358
Add tests for component utilities: forwardRef2, memo2, and Children u…
ajayi-joseph Jan 2, 2026
cb37276
Add tests for component composition functionality
ajayi-joseph Jan 2, 2026
fa44e8b
Add tests for conditional and list rendering functionality
ajayi-joseph Jan 2, 2026
6a508d0
Add tests for HTML elements rendering functionality
ajayi-joseph Jan 2, 2026
4aee923
Add tests for special components: Fragment and StrictMode, and utilit…
ajayi-joseph Jan 2, 2026
35e5a33
Add tests for event handling functionality including click, focus, an…
ajayi-joseph Jan 2, 2026
ae63da6
Add tests for createRef functionality
ajayi-joseph Jan 2, 2026
2feae55
Add tests for useCallback hook functionality
ajayi-joseph Jan 2, 2026
b406bf6
Add tests for useContext hook functionality
ajayi-joseph Jan 2, 2026
2cf99dd
Add tests for useLayoutEffect hook functionality
ajayi-joseph Jan 2, 2026
72f3a4e
Add tests for useEffect hook functionality
ajayi-joseph Jan 2, 2026
5494605
Add tests for useMemo hook functionality
ajayi-joseph Jan 2, 2026
92c4138
Add tests for useReducerLazy hook functionality
ajayi-joseph Jan 2, 2026
34131eb
Add tests for useReducer hook functionality
ajayi-joseph Jan 2, 2026
c97af8e
Add tests for useRef hook functionality
ajayi-joseph Jan 2, 2026
222a96d
Add tests for useStateJSArray hook functionality
ajayi-joseph Jan 2, 2026
d9b7988
Add tests for useStateLazy hook functionality
ajayi-joseph Jan 2, 2026
3cf9767
Add tests for useState hook functionality
ajayi-joseph Jan 2, 2026
1348b8c
Add tests for JSX DSL functionality
ajayi-joseph Jan 2, 2026
d5e688a
Remove use_state_js_test after break down
ajayi-joseph Jan 2, 2026
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
152 changes: 152 additions & 0 deletions packages/dart_node_react/test/components/component_utilities_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/// Tests for component utilities (forwardRef, memo, Children).
@TestOn('js')
library;

import 'dart:js_interop';

import 'package:dart_node_react/dart_node_react.dart' hide RenderResult, render;
import 'package:dart_node_react/src/testing_library.dart';
import 'package:test/test.dart';

void main() {
group('forwardRef2', () {
test('forwards ref to child component', () {
final fancyInput = forwardRef2(
(props, ref) => input(
type: 'text',
placeholder: props['placeholder'] as String? ?? '',
props: {'ref': ref, 'data-testid': 'fancy-input'},
),
);

final result = render(
createElement(fancyInput, createProps({'placeholder': 'Enter text'})),
);

final inputEl = result.getByTestId('fancy-input');
expect(inputEl.getAttribute('placeholder'), equals('Enter text'));

result.unmount();
});
});

group('memo2', () {
test('prevents unnecessary re-renders', () {
var childRenderCount = 0;

final child = registerFunctionComponent((props) {
childRenderCount++;
return pEl('Name: ${props['name']}', props: {'data-testid': 'child'});
});

final memoizedChild = memo2(child);

final parent = registerFunctionComponent((props) {
final count = useState(0);
return div(
children: [
pEl('Parent count: ${count.value}'),
createElement(memoizedChild, createProps({'name': 'Alice'})),
button(
text: 'Inc Parent',
props: {'data-testid': 'inc'},
onClick: () => count.set(count.value + 1),
),
],
);
});

childRenderCount = 0;
final result = render(fc(parent));

final initialRenders = childRenderCount;

fireClick(result.getByTestId('inc'));

expect(childRenderCount, equals(initialRenders));

result.unmount();
});

test('re-renders when props change with custom comparison', () {
var renderCount = 0;

final child = registerFunctionComponent((props) {
renderCount++;
return pEl(
'ID: ${props['id']}, Name: ${props['name']}',
props: {'data-testid': 'child'},
);
});

final memoizedChild = memo2(
child,
arePropsEqual: (prev, next) => prev['id'] == next['id'],
);

final parent = registerFunctionComponent((props) {
final id = useState(1);
final name = useState('Alice');
return div(
children: [
createElement(
memoizedChild,
createProps({'id': id.value, 'name': name.value}),
),
button(
text: 'Change Name',
props: {'data-testid': 'change-name'},
onClick: () => name.set('Bob'),
),
button(
text: 'Change ID',
props: {'data-testid': 'change-id'},
onClick: () => id.set(id.value + 1),
),
],
);
});

renderCount = 0;
final result = render(fc(parent));

final initial = renderCount;

fireClick(result.getByTestId('change-name'));
expect(renderCount, equals(initial));

fireClick(result.getByTestId('change-id'));
expect(renderCount, greaterThan(initial));

result.unmount();
});
});

group('Children utilities', () {
test('Children.count works with null children', () {
final wrapper = registerFunctionComponent((props) {
final children = props['children'] as JSAny?;
final count = Children.count(children);
return pEl('Count: $count', props: {'data-testid': 'count'});
});

// Pass no children - count should be 0
final result = render(fc(wrapper));

expect(result.getByTestId('count').textContent, equals('Count: 0'));

result.unmount();
});

test('Children utilities are available for import', () {
// Simple test to verify Children utilities compile and are accessible
// The count function exists and works with null
final count = Children.count(null);
expect(count, equals(0));

// toArray with null returns empty list
final arr = Children.toArray(null);
expect(arr, isEmpty);
});
});
}
77 changes: 77 additions & 0 deletions packages/dart_node_react/test/components/composition_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/// Tests for component composition functionality.
@TestOn('js')
library;

import 'package:dart_node_react/dart_node_react.dart' hide RenderResult, render;
import 'package:dart_node_react/src/testing_library.dart';
import 'package:test/test.dart';

void main() {
test('parent passes props to child', () {
final child = registerFunctionComponent(
(props) =>
pEl('Hello, ${props['name']}!', props: {'data-testid': 'greeting'}),
);

final parent = registerFunctionComponent(
(props) => div(
children: [
fc(child, {'name': 'World'}),
],
),
);

final result = render(fc(parent));

expect(result.getByTestId('greeting').textContent, equals('Hello, World!'));

result.unmount();
});

test('child calls parent callback', () {
var parentNotified = false;

final child = registerFunctionComponent((props) {
final onNotify = props['onNotify'] as void Function()?;
return button(
text: 'Notify',
onClick: onNotify,
props: {'data-testid': 'notify'},
);
});

final parent = registerFunctionComponent(
(props) => fc(child, {'onNotify': () => parentNotified = true}),
);

final result = render(fc(parent));

expect(parentNotified, isFalse);

fireClick(result.getByTestId('notify'));

expect(parentNotified, isTrue);

result.unmount();
});

test('deeply nested components work correctly', () {
final grandChild = registerFunctionComponent(
(props) => span('GrandChild', props: {'data-testid': 'grandchild'}),
);

final child = registerFunctionComponent(
(props) => div(children: [fc(grandChild)]),
);

final parent = registerFunctionComponent(
(props) => div(children: [fc(child)]),
);

final result = render(fc(parent));

expect(result.getByTestId('grandchild').textContent, equals('GrandChild'));

result.unmount();
});
}
144 changes: 144 additions & 0 deletions packages/dart_node_react/test/components/rendering_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/// Tests for conditional and list rendering functionality.
@TestOn('js')
library;

import 'package:dart_node_react/dart_node_react.dart' hide RenderResult, render;
import 'package:dart_node_react/src/testing_library.dart';
import 'package:test/test.dart';

void main() {
group('Conditional rendering', () {
test('shows/hides content based on state', () {
final toggle = registerFunctionComponent((props) {
final visible = useState(false);
return div(
children: [
button(
text: visible.value ? 'Hide' : 'Show',
onClick: () => visible.set(!visible.value),
props: {'data-testid': 'toggle'},
),
if (visible.value)
pEl('Content', props: {'data-testid': 'content'})
else
span(''),
],
);
});

final result = render(fc(toggle));

expect(result.queryByTestId('content'), isNull);

fireClick(result.getByTestId('toggle'));
expect(result.queryByTestId('content'), isNotNull);
expect(result.getByTestId('content').textContent, equals('Content'));

fireClick(result.getByTestId('toggle'));
expect(result.queryByTestId('content'), isNull);

result.unmount();
});

test('switches between components', () {
final switcher = registerFunctionComponent((props) {
final showA = useState(true);
return div(
children: [
button(
text: 'Switch',
onClick: () => showA.set(!showA.value),
props: {'data-testid': 'switch'},
),
if (showA.value)
pEl('Component A', props: {'data-testid': 'a'})
else
pEl('Component B', props: {'data-testid': 'b'}),
],
);
});

final result = render(fc(switcher));

expect(result.queryByTestId('a'), isNotNull);
expect(result.queryByTestId('b'), isNull);

fireClick(result.getByTestId('switch'));

expect(result.queryByTestId('a'), isNull);
expect(result.queryByTestId('b'), isNotNull);

result.unmount();
});
});

group('List rendering', () {
test('renders static list of items', () {
// Static list that doesn't rely on state
final itemList = registerFunctionComponent((props) {
// Use static data passed via props
final itemsStr = props['items'] as String? ?? 'Apple,Banana,Cherry';
final items = itemsStr.split(',');
return ul(
props: {'data-testid': 'list'},
children: items
.map((item) => li(item, props: {'key': item}))
.toList(),
);
});

final result = render(fc(itemList, {'items': 'Apple,Banana,Cherry'}));

final list = result.getByTestId('list');
expect(list.innerHTML, contains('Apple'));
expect(list.innerHTML, contains('Banana'));
expect(list.innerHTML, contains('Cherry'));

result.unmount();
});

test('adds and removes items via string state', () {
// Use comma-separated string for list state to work with JS interop
final dynamicList = registerFunctionComponent((props) {
final itemsStr = useState('One');
final items = itemsStr.value.split(',').where((s) => s.isNotEmpty);
return div(
children: [
ul(
props: {'data-testid': 'list'},
children: items.map(li).toList(),
),
button(
text: 'Add',
onClick: () => itemsStr.set('${itemsStr.value},New'),
props: {'data-testid': 'add'},
),
button(
text: 'Remove',
onClick: () {
final parts = itemsStr.value.split(',');
final newValue = parts.length > 1
? parts.sublist(0, parts.length - 1).join(',')
: '';
itemsStr.set(newValue);
},
props: {'data-testid': 'remove'},
),
],
);
});

final result = render(fc(dynamicList));

expect(result.getByTestId('list').innerHTML, contains('One'));

fireClick(result.getByTestId('add'));
expect(result.getByTestId('list').innerHTML, contains('New'));

fireClick(result.getByTestId('remove'));
expect(result.getByTestId('list').innerHTML, isNot(contains('New')));

result.unmount();
});
});
}
Loading