Skip to content

Commit 7419e8b

Browse files
committed
tests: misc
1 parent 3d42c58 commit 7419e8b

23 files changed

+1815
-350
lines changed

lib/src/models/feed/feed_item_action.dart

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ FeedItemAction feedItemActionFromJson(Map<String, dynamic> json) {
4646
}
4747
}
4848

49-
/// Helper function to serialize a [FeedItemAction] instance to a JSON map.
49+
/// Helper function tDeefItem.type and Source.sourceTpe are nt symentically in conflicet, why updting itemAction] instance to a JSON map.
5050
///
5151
/// This simply calls the `toJson()` method on the concrete [FeedItemAction]
5252
/// instance.
@@ -63,7 +63,11 @@ class OpenInternalContent extends FeedItemAction {
6363
const OpenInternalContent({
6464
required this.contentId,
6565
required this.contentType,
66-
});
66+
}) : type = 'open_internal_content';
67+
68+
/// A string representation of the action type.
69+
@JsonKey(name: 'type', required: true)
70+
final String type;
6771

6872
/// Factory method to create an [OpenInternalContent] instance from a JSON map.
6973
factory OpenInternalContent.fromJson(Map<String, dynamic> json) =>
@@ -88,7 +92,11 @@ class ShowInterstitialThenOpenInternalContent extends FeedItemAction {
8892
const ShowInterstitialThenOpenInternalContent({
8993
required this.contentId,
9094
required this.contentType,
91-
});
95+
}) : type = 'show_interstitial_then_open_internal_content';
96+
97+
/// A string representation of the action type.
98+
@JsonKey(name: 'type', required: true)
99+
final String type;
92100

93101
/// Factory method to create a [ShowInterstitialThenOpenInternalContent]
94102
/// instance from a JSON map.
@@ -114,7 +122,11 @@ class ShowInterstitialThenOpenInternalContent extends FeedItemAction {
114122
@JsonSerializable()
115123
class OpenExternalUrl extends FeedItemAction {
116124
/// {@macro open_external_url}
117-
const OpenExternalUrl({required this.url});
125+
const OpenExternalUrl({required this.url}) : type = 'open_external_url';
126+
127+
/// A string representation of the action type.
128+
@JsonKey(name: 'type', required: true)
129+
final String type;
118130

119131
/// Factory method to create an [OpenExternalUrl] instance from a JSON map.
120132
factory OpenExternalUrl.fromJson(Map<String, dynamic> json) =>

lib/src/models/news/source.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Source extends FeedItem {
1818
/// {@macro source}
1919
Source({
2020
required this.name,
21-
required super.action,
21+
required this.action,
2222
this.description,
2323
this.url,
2424
SourceType? sourceType, // Renamed to avoid conflict with FeedItem.type
@@ -27,7 +27,7 @@ class Source extends FeedItem {
2727
String? id,
2828
}) : id = id ?? const Uuid().v4(),
2929
_sourceType = sourceType,
30-
super(type: 'source');
30+
super(type: 'source', action: action);
3131

3232
/// Factory method to create a [Source] instance from a JSON map.
3333
factory Source.fromJson(Map<String, dynamic> json) => _$SourceFromJson(json);
@@ -48,7 +48,7 @@ class Source extends FeedItem {
4848
/// The type of the source (e.g., newsAgency, blog).
4949
/// If an unknown value is encountered during deserialization,
5050
/// this field will be set to null.
51-
@JsonKey(name: 'type', unknownEnumValue: JsonKey.nullForUndefinedEnumValue)
51+
@JsonKey(name: 'sourceType', unknownEnumValue: JsonKey.nullForUndefinedEnumValue)
5252
final SourceType? _sourceType;
5353

5454
/// Public getter for the source type.
@@ -62,8 +62,7 @@ class Source extends FeedItem {
6262

6363
/// The action to be performed when this feed item is interacted with.
6464
@JsonKey(fromJson: feedItemActionFromJson, toJson: feedItemActionToJson)
65-
@override
66-
late final FeedItemAction action;
65+
final FeedItemAction action;
6766

6867
/// Converts this [Source] instance to a JSON map.
6968
@override
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import 'package:ht_shared/src/models/content_type.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
group('ContentType', () {
6+
test('has correct string values for JSON serialization', () {
7+
expect(ContentType.headline.name, 'headline');
8+
expect(ContentType.category.name, 'category');
9+
expect(ContentType.source.name, 'source');
10+
expect(ContentType.country.name, 'country');
11+
});
12+
13+
test('can be created from string values', () {
14+
expect(ContentType.values.byName('headline'), ContentType.headline);
15+
expect(ContentType.values.byName('category'), ContentType.category);
16+
expect(ContentType.values.byName('source'), ContentType.source);
17+
expect(ContentType.values.byName('country'), ContentType.country);
18+
});
19+
20+
test('has correct toString representation', () {
21+
expect(ContentType.headline.toString(), 'ContentType.headline');
22+
expect(ContentType.category.toString(), 'ContentType.category');
23+
expect(ContentType.source.toString(), 'ContentType.source');
24+
expect(ContentType.country.toString(), 'ContentType.country');
25+
});
26+
});
27+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'package:ht_shared/src/models/feed/ad_placement.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
group('AdPlacement', () {
6+
test('has correct string values for JSON serialization', () {
7+
expect(
8+
AdPlacement.feedInlineStandardBanner.name,
9+
'feedInlineStandardBanner',
10+
);
11+
expect(
12+
AdPlacement.feedInlineNativeBanner.name,
13+
'feedInlineNativeBanner',
14+
);
15+
});
16+
17+
test('can be created from string values', () {
18+
expect(
19+
AdPlacement.values.byName('feedInlineStandardBanner'),
20+
AdPlacement.feedInlineStandardBanner,
21+
);
22+
expect(
23+
AdPlacement.values.byName('feedInlineNativeBanner'),
24+
AdPlacement.feedInlineNativeBanner,
25+
);
26+
});
27+
28+
test('has correct toString representation', () {
29+
expect(
30+
AdPlacement.feedInlineStandardBanner.toString(),
31+
'AdPlacement.feedInlineStandardBanner',
32+
);
33+
expect(
34+
AdPlacement.feedInlineNativeBanner.toString(),
35+
'AdPlacement.feedInlineNativeBanner',
36+
);
37+
});
38+
});
39+
}

test/src/models/feed/ad_test.dart

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import 'package:ht_shared/src/models/feed/ad.dart';
2+
import 'package:ht_shared/src/models/content_type.dart';
3+
import 'package:ht_shared/src/models/feed/ad_placement.dart';
4+
import 'package:ht_shared/src/models/feed/feed_item_action.dart';
5+
import 'package:test/test.dart';
6+
import 'package:uuid/uuid.dart';
7+
8+
void main() {
9+
group('Ad', () {
10+
const testId = 'test-ad-id';
11+
const testImageUrl = 'http://example.com/ad.jpg';
12+
const testTargetUrl = 'http://example.com/target';
13+
const testAdType = AdType.banner;
14+
const testPlacement = AdPlacement.feedInlineStandardBanner;
15+
const defaultAction = OpenExternalUrl(url: 'http://default.com');
16+
17+
Ad createSubject({
18+
String? id,
19+
String imageUrl = testImageUrl,
20+
String targetUrl = testTargetUrl,
21+
AdType adType = testAdType,
22+
AdPlacement? placement = testPlacement,
23+
FeedItemAction action = defaultAction,
24+
}) {
25+
return Ad(
26+
id: id,
27+
imageUrl: imageUrl,
28+
targetUrl: targetUrl,
29+
adType: adType,
30+
placement: placement,
31+
action: action,
32+
);
33+
}
34+
35+
group('constructor', () {
36+
test('generates id when not provided', () {
37+
final ad = createSubject(id: null);
38+
expect(ad.id, isA<String>());
39+
expect(Uuid.isValidUUID(fromString: ad.id), isTrue);
40+
});
41+
42+
test('uses provided id', () {
43+
final ad = createSubject(id: testId);
44+
expect(ad.id, testId);
45+
});
46+
47+
test('initializes all properties correctly', () {
48+
final ad = createSubject();
49+
expect(ad.imageUrl, testImageUrl);
50+
expect(ad.targetUrl, testTargetUrl);
51+
expect(ad.adType, testAdType);
52+
expect(ad.placement, testPlacement);
53+
expect(ad.action, defaultAction);
54+
expect(ad.type, 'ad');
55+
});
56+
});
57+
58+
group('copyWith', () {
59+
test('returns a new instance with updated fields', () {
60+
const newImageUrl = 'http://new.com/ad.png';
61+
const newAdType = AdType.video;
62+
const newAction = OpenInternalContent(
63+
contentId: 'new-content',
64+
contentType: ContentType.headline,
65+
);
66+
67+
final originalAd = createSubject();
68+
final updatedAd = originalAd.copyWith(
69+
imageUrl: newImageUrl,
70+
adType: newAdType,
71+
action: newAction,
72+
);
73+
74+
expect(updatedAd.id, originalAd.id);
75+
expect(updatedAd.imageUrl, newImageUrl);
76+
expect(updatedAd.targetUrl, originalAd.targetUrl);
77+
expect(updatedAd.adType, newAdType);
78+
expect(updatedAd.placement, originalAd.placement);
79+
expect(updatedAd.action, newAction);
80+
expect(updatedAd.type, originalAd.type);
81+
});
82+
83+
test('returns an identical copy if no updates provided', () {
84+
final originalAd = createSubject();
85+
final copiedAd = originalAd.copyWith();
86+
expect(copiedAd, originalAd);
87+
expect(identical(copiedAd, originalAd), isFalse);
88+
});
89+
});
90+
91+
group('toJson', () {
92+
test('serializes full Ad object to JSON', () {
93+
final ad = createSubject();
94+
final json = ad.toJson();
95+
96+
expect(json, <String, dynamic>{
97+
'id': ad.id,
98+
'imageUrl': testImageUrl,
99+
'targetUrl': testTargetUrl,
100+
'adType': testAdType.name,
101+
'placement': testPlacement.name,
102+
'type': 'ad',
103+
'action': defaultAction.toJson(),
104+
});
105+
});
106+
107+
test('omits null optional fields from JSON', () {
108+
final ad = createSubject(placement: null);
109+
final json = ad.toJson();
110+
111+
expect(json.containsKey('placement'), isFalse);
112+
});
113+
});
114+
115+
group('fromJson', () {
116+
test('deserializes full JSON to Ad object', () {
117+
final json = <String, dynamic>{
118+
'id': testId,
119+
'imageUrl': testImageUrl,
120+
'targetUrl': testTargetUrl,
121+
'adType': testAdType.name,
122+
'placement': testPlacement.name,
123+
'type': 'ad',
124+
'action': defaultAction.toJson(),
125+
};
126+
final ad = Ad.fromJson(json);
127+
128+
expect(ad.id, testId);
129+
expect(ad.imageUrl, testImageUrl);
130+
expect(ad.targetUrl, testTargetUrl);
131+
expect(ad.adType, testAdType);
132+
expect(ad.placement, testPlacement);
133+
expect(ad.action, defaultAction);
134+
expect(ad.type, 'ad');
135+
});
136+
137+
test('deserializes JSON with missing optional fields', () {
138+
final json = <String, dynamic>{
139+
'id': testId,
140+
'imageUrl': testImageUrl,
141+
'targetUrl': testTargetUrl,
142+
'adType': testAdType.name,
143+
'type': 'ad',
144+
'action': defaultAction.toJson(),
145+
};
146+
final ad = Ad.fromJson(json);
147+
148+
expect(ad.placement, isNull);
149+
});
150+
151+
test('deserializes JSON with unknown adType gracefully', () {
152+
final json = <String, dynamic>{
153+
'id': testId,
154+
'imageUrl': testImageUrl,
155+
'targetUrl': testTargetUrl,
156+
'adType': 'unknown_type',
157+
'type': 'ad',
158+
'action': defaultAction.toJson(),
159+
};
160+
final ad = Ad.fromJson(json);
161+
expect(ad.adType, isNull); // Should be null for unknown enum value
162+
});
163+
});
164+
165+
group('Equatable', () {
166+
test('instances with same properties are equal', () {
167+
final ad1 = createSubject(id: '1');
168+
final ad2 = createSubject(id: '1');
169+
expect(ad1, ad2);
170+
});
171+
172+
test('instances with different properties are not equal', () {
173+
final ad1 = createSubject(id: '1');
174+
final ad2 = createSubject(id: '2');
175+
expect(ad1, isNot(equals(ad2)));
176+
});
177+
178+
test('props list contains all relevant fields', () {
179+
final ad = createSubject();
180+
expect(ad.props, [
181+
ad.id,
182+
ad.imageUrl,
183+
ad.targetUrl,
184+
ad.adType,
185+
ad.placement,
186+
ad.type,
187+
ad.action,
188+
]);
189+
});
190+
});
191+
});
192+
}

0 commit comments

Comments
 (0)