diff --git a/CHANGELOG.md b/CHANGELOG.md index 770d1e1..d855b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## 3.0.16 + +- `UILayoutEvaluator`: + - Fixed a null-check crash when a layout expression referenced an unresolvable element or + `this` target (e.g. `#missing.center.x`). `_evalMemberExpressionImpl` and `_evalThisImpl` + now return `null` instead of force-unwrapping a null resolved expression, so `processLayout` + correctly falls back to its `defaultValue`. + +- `lib/src/component/capture.dart`: + - `UICapture.getInputFile`: hardened against a null `HTMLInputElement.files`, returning `null` + instead of throwing (matching the null-safe handling already used in `_readFile`). + +- Tests: + - Added `test/capture_test.dart` covering the `UICapture` data-format conversion matrix + (`arrayBuffer`/`base64`/`string`/`dataUrlBase64`), field value round-trips, `null` clearing + and `latin1` encoding. + - Expanded `test/layout_expression_test.dart` with unit arithmetic, fallback-unit application, + mixed-unit rejection and the unresolvable-element fallback (regression test for the fix above). + ## 3.0.15 - `lib/src/component/capture.dart`: diff --git a/lib/src/bones_ui.dart b/lib/src/bones_ui.dart index 6579a02..5e9d66c 100644 --- a/lib/src/bones_ui.dart +++ b/lib/src/bones_ui.dart @@ -1,3 +1,3 @@ class BonesUI { - static const String version = '3.0.15'; + static const String version = '3.0.16'; } diff --git a/lib/src/bones_ui_layout.dart b/lib/src/bones_ui_layout.dart index ec66c8a..27642b7 100644 --- a/lib/src/bones_ui_layout.dart +++ b/lib/src/bones_ui_layout.dart @@ -320,8 +320,10 @@ class UILayoutEvaluator extends ExpressionEvaluator { if (expressionResolved is ElementExpression) { return evalElementExpression(expressionResolved, context); + } else if (expressionResolved == null) { + return null; } else { - return eval(expressionResolved!, context); + return eval(expressionResolved, context); } } @@ -413,8 +415,10 @@ class UILayoutEvaluator extends ExpressionEvaluator { } return evaluated; } + } else if (expressionResolved == null) { + return null; } else { - return eval(expressionResolved!, context); + return eval(expressionResolved, context); } } diff --git a/lib/src/component/capture.dart b/lib/src/component/capture.dart index 5626121..4634afb 100644 --- a/lib/src/component/capture.dart +++ b/lib/src/component/capture.dart @@ -612,8 +612,9 @@ abstract class UICapture extends UIButtonBase implements UIField { File? getInputFile() { var input = getInputCapture() as HTMLInputElement?; if (input == null) return null; - var files = input.files!; - return files.isNotEmpty ? files.item(0) : null; + var files = input.files; + if (files == null || files.isEmpty) return null; + return files.item(0); } bool isFileImage() { diff --git a/pubspec.yaml b/pubspec.yaml index 67c4fdf..8cb3d95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bones_ui description: Bones_UI - An intuitive and user-friendly Web User Interface framework for Dart. -version: 3.0.15 +version: 3.0.16 homepage: https://github.com/Colossus-Services/bones_ui environment: diff --git a/test/capture_test.dart b/test/capture_test.dart new file mode 100644 index 0000000..73c2442 --- /dev/null +++ b/test/capture_test.dart @@ -0,0 +1,96 @@ +@TestOn('browser') +library; + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:bones_ui/bones_ui_test.dart'; +import 'package:test/test.dart'; + +void main() { + group('UICapture conversions', () { + late final _CaptureRoot uiRoot; + + setUpAll(() async { + uiRoot = await initializeTestUIRoot((rootContainer) { + return _CaptureRoot(rootContainer); + }); + await uiRoot.callRenderAndWait(); + }); + + // "hi" -> bytes [104, 105] -> base64 "aGk=" + final bytes = Uint8List.fromList([104, 105]); + const base64Str = 'aGk='; + + UIButtonCapturePhoto newCapture(CaptureDataFormat format) { + var capture = UIButtonCapturePhoto( + uiRoot.content, + text: 'Capture', + captureDataFormat: format, + ); + return capture; + } + + test('arrayBuffer format round-trips', () { + var capture = newCapture(CaptureDataFormat.arrayBuffer); + capture.selectedFileData = bytes; + + expect(capture.hasSelectedFileData, isTrue); + expect(capture.selectedFileDataAsArrayBuffer, equals(bytes)); + expect(capture.selectedFileDataAsBase64, equals(base64Str)); + expect(capture.selectedFileDataAsString, equals('hi')); + }); + + test('base64 format round-trips', () { + var capture = newCapture(CaptureDataFormat.base64); + capture.selectedFileData = base64Str; + + expect(capture.hasSelectedFileData, isTrue); + expect(capture.selectedFileDataAsBase64, equals(base64Str)); + expect(capture.selectedFileDataAsArrayBuffer, equals(bytes)); + }); + + test('null clears the data', () { + var capture = newCapture(CaptureDataFormat.arrayBuffer); + capture.selectedFileData = bytes; + expect(capture.hasSelectedFileData, isTrue); + + capture.setFieldValue(null); + expect(capture.hasSelectedFileData, isFalse); + expect(capture.getFieldValue(), isNull); + }); + + test('dataUrlBase64 format exposes a data URL', () { + var capture = newCapture(CaptureDataFormat.dataUrlBase64); + capture.selectedFileData = 'data:text/plain;base64,$base64Str'; + + var url = capture.selectedFileDataAsURLOrDataURLBase64; + expect(url, startsWith('data:')); + expect(url, contains(base64Str)); + expect(capture.selectedFileDataAsString, equals('hi')); + }); + + test('fieldValue round-trips via setFieldValue/getFieldValue', () { + var capture = newCapture(CaptureDataFormat.base64); + capture.setFieldValue(base64Str); + expect(capture.getFieldValue(), isNotNull); + expect(capture.selectedFileDataAsArrayBuffer, equals(bytes)); + }); + + test('latin1 encoding applied to string<->bytes', () { + var capture = newCapture(CaptureDataFormat.string); + capture.setDataEncodingToLatin1(); + // 0xE9 is 'é' in latin1 (invalid as standalone UTF-8). + capture.selectedFileData = Uint8List.fromList([0xE9]); + expect(capture.dataEncoding, equals(latin1)); + expect(capture.selectedFileDataAsString, equals('é')); + }); + }); +} + +class _CaptureRoot extends UIRoot { + _CaptureRoot(super.rootContainer) : super(id: 'CaptureRoot'); + + @override + UIComponent? renderContent() => null; +} diff --git a/test/layout_expression_test.dart b/test/layout_expression_test.dart index 8f43857..41eff58 100644 --- a/test/layout_expression_test.dart +++ b/test/layout_expression_test.dart @@ -50,5 +50,49 @@ void main() { expect(r, equals('11px')); expect(elementsAccess, equals(['foo', '_'])); }); + + UILayoutEvaluator newEvaluator() => UILayoutEvaluator( + (String id, bool all) => null, + (dynamic elem, String property) => elem is Map ? elem[property] : null, + ); + + test('unit arithmetic (same unit)', () { + var evaluator = newEvaluator(); + expect(evaluator.processLayout('10px + 5px', {}), equals('15px')); + expect(evaluator.processLayout('20px - 8px', {}), equals('12px')); + expect(evaluator.processLayout('10px * 3', {}), equals('30px')); + }); + + test('unit arithmetic with context variable', () { + var evaluator = newEvaluator(); + var r = evaluator.processLayout('10px + x', {'x': 5}); + expect(r, equals('15px')); + }); + + test('applies fallback unit to plain number result', () { + var evaluator = newEvaluator(); + expect(evaluator.processLayout('2 + 3', {}, 'px'), equals('5px')); + // A value that already carries a unit must not get a second one. + expect(evaluator.processLayout('2px + 3px', {}, 'px'), equals('5px')); + }); + + test('mixed units are rejected', () { + var evaluator = newEvaluator(); + expect( + () => evaluator.processLayout('10px + 5em', {}), + throwsUnsupportedError, + ); + }); + + test('returns default value for unresolvable element', () { + var evaluator = newEvaluator(); + var r = evaluator.processLayout( + '#missing.center.x', + {}, + 'px', + 'fallback', + ); + expect(r, equals('fallback')); + }); }); }