Skip to content
Merged
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`:
Expand Down
2 changes: 1 addition & 1 deletion lib/src/bones_ui.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class BonesUI {
static const String version = '3.0.15';
static const String version = '3.0.16';
}
8 changes: 6 additions & 2 deletions lib/src/bones_ui_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
5 changes: 3 additions & 2 deletions lib/src/component/capture.dart
Original file line number Diff line number Diff line change
Expand Up @@ -612,8 +612,9 @@ abstract class UICapture extends UIButtonBase implements UIField<String> {
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() {
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
96 changes: 96 additions & 0 deletions test/capture_test.dart
Original file line number Diff line number Diff line change
@@ -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;
}
44 changes: 44 additions & 0 deletions test/layout_expression_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
});
}
Loading