Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: update tests #23

Merged
merged 7 commits into from
Dec 24, 2024
37 changes: 37 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Test

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y lcov
flutter pub get
dart pub global activate coverage
dart pub global activate melos
melos bootstrap

- name: Run tests with coverage
run: |
melos run coverage
melos run combine-coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: coverage/lcov.info
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Transform HTML markup into native Flutter widgets with an elegant, customizable

[![pub package](https://img.shields.io/pub/v/tagflow.svg?label=tagflow&color=orange)](https://pub.dev/packages/tagflow)
[![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg)](https://pub.dev/packages/very_good_analysis)
[![codecov](https://codecov.io/gh/devaryakjha/tagflow/branch/main/graph/badge.svg)](https://codecov.io/gh/devaryakjha/tagflow)

---

Expand Down
4 changes: 3 additions & 1 deletion examples/tagflow/lib/screens/article_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ const articleHtml = r'''

<h3>Code Example</h3>

<pre><code>// Simple React component
<pre>
<code>// Simple React component
function Welcome(props) {
return React.createElement('h1', null, `Hello, ${props.name}`);
}</code></pre>
Expand Down Expand Up @@ -69,6 +70,7 @@ final class ArticleExample extends ExamplePage {
baseTextStyle: theme.textTheme.bodyMedium!,
headingTextStyle: theme.textTheme.headlineMedium!,
codeTextStyle: codeTextTheme.bodyMedium,
codeBackground: theme.colorScheme.surfaceContainerHigh,
resolveAdditionalStyles: (theme) {
final blockquoteStyle =
theme.styles['blockquote'] ?? const TagflowStyle();
Expand Down
12 changes: 11 additions & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,14 @@ scripts:
run: |
melos clean && \
melos bootstrap
description: Clean and bootstrap workspace
description: Clean and bootstrap workspace

coverage:
run: melos exec -c 1 --fail-fast -- "flutter test --coverage"
description: Run tests with coverage and generate coverage report

combine-coverage:
run: |
mkdir -p coverage
lcov --add-tracefile packages/tagflow/coverage/lcov.info -o coverage/lcov.info
description: Combine coverage reports from all packages
83 changes: 6 additions & 77 deletions packages/tagflow/lib/src/converter/converters/code_converter.dart
Original file line number Diff line number Diff line change
@@ -1,87 +1,16 @@
import 'package:flutter/material.dart';
import 'package:tagflow/tagflow.dart';

/// A converter for code elements (`code` and `pre` tags)
final class CodeConverter extends ElementConverter {
final class CodeConverter extends TextConverter {
/// Create a new code converter
const CodeConverter();

@override
Set<String> get supportedTags => {'code', 'pre'};

@override
Widget convert(
TagflowElement element,
BuildContext context,
TagflowConverter converter,
) {
// Handle pre tag (code block)
if (element.tag == 'pre') {
return _buildPreElement(element, context, converter);
}

// Handle inline code
return _buildInlineCode(element, context, converter);
bool shouldForceWidgetSpan(TagflowElement element) {
return super.shouldForceWidgetSpan(element) ||
['code', 'pre'].contains(element.tag);
}

Widget _buildPreElement(
TagflowElement element,
BuildContext context,
TagflowConverter converter,
) {
final style = resolveStyle(element, context);

// Pre elements typically contain a code element
final codeElement = element.children.firstWhere(
(e) => e.tag == 'code',
orElse: () => element,
);

final children = converter.convertChildren(codeElement.children, context);

return StyledContainer(
style: style,
tag: element.tag,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}

Widget _buildInlineCode(
TagflowElement element,
BuildContext context,
TagflowConverter converter,
) {
final style = resolveStyle(element, context);

final children = converter.convertChildren(element.children, context);

// Handle single text child more efficiently
if (children.length == 1 && children.first is Text) {
final text = children.first as Text;
return StyledContainer(
style: style,
tag: element.tag,
child: Text(
text.data ?? '',
style: style.textStyle,
textAlign: style.textAlign,
),
);
}

// Handle multiple or non-text children
return StyledContainer(
style: style,
tag: element.tag,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}
@override
Set<String> get supportedTags => super.supportedTags.union({'code', 'pre'});
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ class TextConverter extends ElementConverter {
return null;
}

bool shouldForceWidgetSpan(TagflowElement element) {
return ['sub', 'sup', 'mark'].contains(element.tag);
}

List<InlineSpan> _convertChildren(
TagflowElement element,
BuildContext context,
Expand All @@ -104,7 +108,7 @@ class TextConverter extends ElementConverter {
mouseCursor: _getMouseCursor(child, context),
);
} else {
if (!canHandle(child) || ['sub', 'sup', 'mark'].contains(child.tag)) {
if (!canHandle(child) || shouldForceWidgetSpan(child)) {
// create a widget span for unsupported elements
return WidgetSpan(
child: converter.convert(child, context),
Expand Down
7 changes: 7 additions & 0 deletions packages/tagflow/lib/src/style/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ class TagflowTheme extends Equatable {
color: codeBackground?.withValues(alpha: 0.5) ?? Colors.grey,
),
textStyle: codeTextStyle,
width: maxWidth ?? double.infinity,
),
'code': TagflowStyle(
textStyle: codeTextStyle ??
Expand All @@ -335,6 +336,12 @@ class TagflowTheme extends Equatable {
horizontal: baseFontSize * 0.25,
vertical: baseFontSize * 0.125,
),
width: maxWidth ?? double.infinity,
),
'pre code': const TagflowStyle(
backgroundColor: Colors.transparent,
padding: EdgeInsets.zero,
margin: EdgeInsets.zero,
),
'li': TagflowStyle(
margin: EdgeInsets.only(bottom: baseFontSize),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tagflow/tagflow.dart';

void main() {
group('ListConverter', () {
test('supports ul and ol tags', () {
const converter = ListConverter();
expect(converter.supportedTags, {'ul', 'ol'});
});

testWidgets('renders unordered list', (tester) async {
final element = TagflowElement(
tag: 'ul',
children: [
TagflowElement(tag: 'li', textContent: 'Item 1'),
TagflowElement(tag: 'li', textContent: 'Item 2'),
],
);

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TagflowThemeProvider(
theme: const TagflowTheme.raw(
defaultStyle: TagflowStyle(),
styles: {},
),
child: Builder(
builder: (context) {
final widget = TagflowConverter().convert(element, context);
return widget;
},
),
),
),
),
);

expect(find.textContaining('Item 1', findRichText: true), findsOneWidget);
expect(find.textContaining('Item 2', findRichText: true), findsOneWidget);
});
});
}
80 changes: 80 additions & 0 deletions packages/tagflow/test/src/core/models/element_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,85 @@ void main() {
element.classList = ['four', 'five'];
expect(element['class'], 'four five');
});

group('link operations', () {
test('identifies anchor elements', () {
final link = TagflowElement(
tag: 'a',
attributes: LinkedHashMap.from({'href': 'https://example.com'}),
);
final div = TagflowElement(tag: 'div');

expect(link.isAnchor, true);
expect(div.isAnchor, false);
expect(link.href, 'https://example.com');
expect(div.href, null);
});

test('handles nested link elements', () {
final parent = TagflowElement(
tag: 'a',
attributes: LinkedHashMap.from({'href': 'https://example.com'}),
);
final child = TagflowElement(tag: 'span');
parent.addChild(child);

expect(child.parentHref, 'https://example.com');
});
});

group('size operations', () {
test('parses width and height attributes', () {
final element = TagflowElement(
tag: 'div',
attributes: LinkedHashMap.from({
'width': '100px',
'height': '50px',
}),
);

expect(element.width, 100.0);
expect(element.height, 50.0);
});

test('handles percentage values', () {
final element = TagflowElement(
tag: 'div',
attributes: LinkedHashMap.from({
'width': '50%',
'height': '25%',
}),
);

expect(element.width, 0.5);
expect(element.height, 0.25);
});

test('handles invalid size values', () {
final element = TagflowElement(
tag: 'div',
attributes: LinkedHashMap.from({
'width': 'invalid',
'height': '',
}),
);

expect(element.width, null);
expect(element.height, null);
});
});

group('style operations', () {
test('parses gap from styles', () {
final element = TagflowElement(
tag: 'div',
attributes: LinkedHashMap.from({
'style': 'gap: 10px',
}),
);

expect(element.gap, 10.0);
});
});
});
}
Loading
Loading