diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml
index 3d061ca..43f954b 100644
--- a/.github/workflows/continuous-integration.yaml
+++ b/.github/workflows/continuous-integration.yaml
@@ -18,3 +18,24 @@ jobs:
with:
flutter_channel: stable
min_coverage: 0
+
+ test-clients:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Git Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: stable
+ cache: true
+
+ - name: Install Dependencies
+ run: flutter pub get
+
+ - name: Run Integration Tests VM
+ run: flutter test test/integration/eventflux_test_vm.dart
+
+ - name: Run Integration Tests Browser
+ run: flutter test test/browser --platform chrome
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4237e4a..bc3620e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
# Changelog 📝
+### v2.3.0-dev.1 🛠️
+This release adds web support 🚀
+- Added `webConfig` parameter to the `connect` method.
+ - This allows you to configure the web client.
+ - Refer README for more info.
+
### v2.2.2-dev.2 🛠️
- Potential fix for multiple connection issue when using single instance method
diff --git a/README.md b/README.md
index 7e965e8..0d76690 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ EventFlux is a Dart package designed for efficient handling of server-sent event
## Supported Platforms
| Android | iOS | Web | MacOS | Windows | Linux |
| ------ | ---- | ---- | ----- | ------- | ----- |
-| ✅|✅|🏗️|✅|❓|❓|
+| ✅|✅|✅|✅|❓|❓|
*Pssst... see those question marks? That's your cue, tech adventurers! Dive in, test, and tell me all about it.* 🚀🛠️
@@ -211,6 +211,7 @@ Connects to a server-sent event stream.
| `tag` | `String` | Optional tag for debugging. | - |
| `logReceivedData` | `bool` | Whether to log received data. | `false` |
| `httpClient` | `HttpClientAdapter?` | Optional Http Client Adapter to allow usage of different http clients. | - |
+| `webConfig` | `WebConfig?` | Allows configuring the web client. Ignored for non-web platforms. | - |
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 55b5367..c694bfd 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -47,7 +47,7 @@ packages:
path: ".."
relative: true
source: path
- version: "2.2.1"
+ version: "2.2.2-dev.2"
fake_async:
dependency: transitive
description:
@@ -56,6 +56,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
+ fetch_api:
+ dependency: transitive
+ description:
+ name: fetch_api
+ sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.0"
+ fetch_client:
+ dependency: transitive
+ description:
+ name: fetch_client
+ sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
flutter:
dependency: "direct main"
description: flutter
diff --git a/lib/client.dart b/lib/client.dart
index 62586b7..abe3d54 100644
--- a/lib/client.dart
+++ b/lib/client.dart
@@ -5,13 +5,16 @@ import 'dart:async';
import 'dart:convert';
import 'package:eventflux/enum.dart';
+import 'package:eventflux/extensions/fetch_client_extension.dart';
import 'package:eventflux/http_client_adapter.dart';
import 'package:eventflux/models/base.dart';
import 'package:eventflux/models/data.dart';
import 'package:eventflux/models/exception.dart';
import 'package:eventflux/models/reconnect.dart';
import 'package:eventflux/models/response.dart';
+import 'package:eventflux/models/web_config/web_config.dart';
import 'package:eventflux/utils.dart';
+import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
/// A class for managing event-driven data streams using Server-Sent Events (SSE).
@@ -24,7 +27,8 @@ class EventFlux extends EventFluxBase {
static final EventFlux _instance = EventFlux._();
static EventFlux get instance => _instance;
- Client? _client;
+ @visibleForTesting
+ Client? client;
StreamController? _streamController;
bool _isExplicitDisconnect = false;
StreamSubscription? _streamSubscription;
@@ -141,7 +145,13 @@ class EventFlux extends EventFluxBase {
bool logReceivedData = false,
List? files,
bool multipartRequest = false,
+
+ /// Optional web config to be used for the connection. Must be provided on web.
+ /// Will be ignored on non-web platforms.
+ WebConfig? webConfig,
}) {
+
+ assert(!(kIsWeb && webConfig == null), 'WebConfig must be provided on web');
// This check prevents redundant connection requests when a connection is already in progress.
// This does not prevent reconnection attempts if autoReconnect is enabled.
@@ -191,6 +201,7 @@ class EventFlux extends EventFluxBase {
logReceivedData: logReceivedData,
files: files,
multipartRequest: multipartRequest,
+ webConfig: webConfig,
);
}
@@ -209,12 +220,14 @@ class EventFlux extends EventFluxBase {
bool logReceivedData = false,
List? files,
bool multipartRequest = false,
+ WebConfig? webConfig,
}) {
/// Initalise variables
/// Create a new HTTP client based on the platform
/// Uses and internal http client if no http client adapter is present
if (httpClient == null) {
- _client = Client();
+ client =
+ kIsWeb ? FetchClientExtension.fromWebConfig(webConfig!) : Client();
}
/// Set `_isExplicitDisconnect` to `false` before connecting.
@@ -272,7 +285,7 @@ class EventFlux extends EventFluxBase {
response = httpClient.send(request);
} else {
// Use internal HTTP client
- response = _client!.send(request);
+ response = client!.send(request);
}
response.then((data) async {
@@ -494,7 +507,7 @@ class EventFlux extends EventFluxBase {
try {
_streamSubscription?.cancel();
_streamController?.close();
- _client?.close();
+ client?.close();
Future.delayed(const Duration(seconds: 1), () {});
eventFluxLog('Disconnected', LogEvent.info, _tag);
_status = EventFluxStatus.disconnected;
@@ -580,25 +593,30 @@ class EventFlux extends EventFluxBase {
break;
case ReconnectMode.exponential:
- _interval = _interval * 2;
- eventFluxLog("Trying again in ${_interval.toString()} seconds",
- LogEvent.reconnect, _tag);
/// It waits for the specified interval before attempting to reconnect.
await Future.delayed(Duration(seconds: _interval), () {
- _start(
- type,
- url,
- onSuccessCallback: onSuccessCallback,
- autoReconnect: autoReconnect,
- onError: onError,
- header: header,
- onConnectionClose: onConnectionClose,
- httpClient: httpClient,
- body: body,
- files: files,
- multipartRequest: multipartRequest,
- );
+ _interval = _interval * 2;
+ if (!isExplicitDisconnect) {
+ eventFluxLog("Trying again in ${_interval.toString()} seconds",
+ LogEvent.reconnect, _tag);
+
+ _status = EventFluxStatus.connectionInitiated;
+ _start(
+ type,
+ url,
+ onSuccessCallback: onSuccessCallback,
+ autoReconnect: autoReconnect,
+ onError: onError,
+ header: header,
+ onConnectionClose: onConnectionClose,
+ httpClient: httpClient,
+ body: body,
+ files: files,
+ multipartRequest: multipartRequest,
+ );
+ }
+
});
break;
}
diff --git a/lib/extensions/fetch_client_extension.dart b/lib/extensions/fetch_client_extension.dart
new file mode 100644
index 0000000..fc64e58
--- /dev/null
+++ b/lib/extensions/fetch_client_extension.dart
@@ -0,0 +1,16 @@
+import 'package:eventflux/models/web_config/web_config.dart';
+import 'package:fetch_client/fetch_client.dart';
+
+extension FetchClientExtension on FetchClient {
+ static FetchClient fromWebConfig(WebConfig webConfig) {
+ return FetchClient(
+ mode: webConfig.mode.toRequestMode(),
+ credentials: webConfig.credentials.toRequestCredentials(),
+ cache: webConfig.cache.toRequestCache(),
+ referrer: webConfig.referrer,
+ referrerPolicy: webConfig.referrerPolicy.toRequestReferrerPolicy(),
+ redirectPolicy: webConfig.redirectPolicy.toRedirectPolicy(),
+ streamRequests: webConfig.streamRequests,
+ );
+ }
+}
diff --git a/lib/models/web_config/redirect_policy.dart b/lib/models/web_config/redirect_policy.dart
new file mode 100644
index 0000000..3ea9a32
--- /dev/null
+++ b/lib/models/web_config/redirect_policy.dart
@@ -0,0 +1,24 @@
+// This code is adapted from fetch_client package
+// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
+// Licensed under the MIT License
+
+import 'package:fetch_client/fetch_client.dart';
+
+/// How requests should handle redirects.
+enum WebConfigRedirectPolicy {
+ /// Default policy - always follow redirects.
+ /// If redirect occurs the only way to know about it is via response properties.
+ alwaysFollow,
+
+ /// Probe via HTTP `GET` request.
+ probe,
+
+ /// Same as [probe] but using `HEAD` method.
+ probeHead;
+
+ RedirectPolicy toRedirectPolicy() => switch (this) {
+ WebConfigRedirectPolicy.alwaysFollow => RedirectPolicy.alwaysFollow,
+ WebConfigRedirectPolicy.probe => RedirectPolicy.probe,
+ WebConfigRedirectPolicy.probeHead => RedirectPolicy.probeHead,
+ };
+}
diff --git a/lib/models/web_config/request_cache.dart b/lib/models/web_config/request_cache.dart
new file mode 100644
index 0000000..46047b0
--- /dev/null
+++ b/lib/models/web_config/request_cache.dart
@@ -0,0 +1,68 @@
+// This code is adapted from fetch_client package
+// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
+// Licensed under the MIT License
+
+import 'package:eventflux/models/web_config/request_mode.dart';
+import 'package:fetch_client/fetch_client.dart';
+
+/// Controls how requests will interact with the browser's HTTP cache.
+enum WebConfigRequestCache {
+ /// The browser looks for a matching request in its HTTP cache.
+ ///
+ /// * If there is a match and it is fresh, it will be returned from the cache.
+ /// * If there is a match but it is stale, the browser will make
+ /// a conditional request to the remote server. If the server indicates
+ /// that the resource has not changed, it will be returned from the cache.
+ /// Otherwise the resource will be downloaded from the server and
+ /// the cache will be updated.
+ /// * If there is no match, the browser will make a normal request,
+ /// and will update the cache with the downloaded resource.
+ byDefault,
+
+ /// The browser fetches the resource from the remote server
+ /// without first looking in the cache, and will not update the cache
+ /// with the downloaded resource.
+ noStore,
+
+ /// The browser fetches the resource from the remote server
+ /// without first looking in the cache, but then will update the cache
+ /// with the downloaded resource.
+ reload,
+
+ /// The browser looks for a matching request in its HTTP cache.
+ ///
+ /// * If there is a match, fresh or stale, the browser will make
+ /// a conditional request to the remote server. If the server indicates
+ /// that the resource has not changed, it will be returned from the cache.
+ /// Otherwise the resource will be downloaded from the server and
+ /// the cache will be updated.
+ /// * If there is no match, the browser will make a normal request,
+ /// and will update the cache with the downloaded resource.
+ noCache,
+
+ /// The browser looks for a matching request in its HTTP cache.
+ ///
+ /// * If there is a match, fresh or stale, it will be returned from the cache.
+ /// * If there is no match, the browser will make a normal request,
+ /// and will update the cache with the downloaded resource.
+ forceCache,
+
+ /// The browser looks for a matching request in its HTTP cache.
+ ///
+ /// * If there is a match, fresh or stale, it will be returned from the cache.
+ /// * If there is no match, the browser will respond
+ /// with a 504 Gateway timeout status.
+ ///
+ /// The [onlyIfCached] mode can only be used if the request's mode
+ /// is [WebConfigRequestMode.sameOrigin].
+ onlyIfCached;
+
+ RequestCache toRequestCache() => switch (this) {
+ WebConfigRequestCache.byDefault => RequestCache.byDefault,
+ WebConfigRequestCache.noStore => RequestCache.noStore,
+ WebConfigRequestCache.reload => RequestCache.reload,
+ WebConfigRequestCache.noCache => RequestCache.noCache,
+ WebConfigRequestCache.forceCache => RequestCache.forceCache,
+ WebConfigRequestCache.onlyIfCached => RequestCache.onlyIfCached,
+ };
+}
diff --git a/lib/models/web_config/request_credentials.dart b/lib/models/web_config/request_credentials.dart
new file mode 100644
index 0000000..a9622cd
--- /dev/null
+++ b/lib/models/web_config/request_credentials.dart
@@ -0,0 +1,27 @@
+// This code is adapted from fetch_client package
+// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
+// Licensed under the MIT License
+
+import 'package:fetch_client/fetch_client.dart';
+
+/// Controls what browsers do with credentials (cookies, HTTP authentication
+/// entries, and TLS client certificates).
+enum WebConfigRequestCredentials {
+ /// Tells browsers to include credentials with requests to same-origin URLs,
+ /// and use any credentials sent back in responses from same-origin URLs.
+ sameOrigin,
+
+ /// Tells browsers to exclude credentials from the request, and ignore
+ /// any credentials sent back in the response (e.g., any Set-Cookie header).
+ omit,
+
+ /// Tells browsers to include credentials in both same- and cross-origin
+ /// requests, and always use any credentials sent back in responses.
+ cors;
+
+ RequestCredentials toRequestCredentials() => switch (this) {
+ WebConfigRequestCredentials.sameOrigin => RequestCredentials.sameOrigin,
+ WebConfigRequestCredentials.omit => RequestCredentials.omit,
+ WebConfigRequestCredentials.cors => RequestCredentials.cors,
+ };
+}
diff --git a/lib/models/web_config/request_mode.dart b/lib/models/web_config/request_mode.dart
new file mode 100644
index 0000000..945fdba
--- /dev/null
+++ b/lib/models/web_config/request_mode.dart
@@ -0,0 +1,46 @@
+// This code is adapted from fetch_client package
+// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
+// Licensed under the MIT License
+
+import 'package:fetch_client/fetch_client.dart';
+
+/// The mode used to determine if cross-origin requests lead to valid responses,
+/// and which properties of the response are readable.
+enum WebConfigRequestMode {
+ /// If a request is made to another origin with this mode set,
+ /// the result is an error. You could use this to ensure that
+ /// a request is always being made to your origin.
+ sameOrigin,
+
+ /// Prevents the method from being anything other than `HEAD`, `GET` or `POST`,
+ /// and the headers from being anything other than simple headers.
+ /// If any `ServiceWorkers` intercept these requests, they may not add
+ /// or override any headers except for those that are simple headers.
+ /// In addition, JavaScript may not access any properties of the resulting
+ /// Response. This ensures that ServiceWorkers do not affect the semantics
+ /// of the Web and prevents security and privacy issues arising from leaking
+ /// data across domains.
+ noCors,
+
+ /// Allows cross-origin requests, for example to access various APIs
+ /// offered by 3rd party vendors. These are expected to adhere to
+ /// the CORS protocol. Only a limited set of headers are exposed
+ /// in the Response, but the body is readable.
+ cors,
+
+ /// A mode for supporting navigation. The navigate value is intended
+ /// to be used only by HTML navigation. A navigate request
+ /// is created only while navigating between documents.
+ navigate,
+
+ /// A special mode used only when establishing a WebSocket connection.
+ webSocket;
+
+ RequestMode toRequestMode() => switch (this) {
+ WebConfigRequestMode.sameOrigin => RequestMode.sameOrigin,
+ WebConfigRequestMode.noCors => RequestMode.noCors,
+ WebConfigRequestMode.cors => RequestMode.cors,
+ WebConfigRequestMode.navigate => RequestMode.navigate,
+ WebConfigRequestMode.webSocket => RequestMode.webSocket,
+ };
+}
diff --git a/lib/models/web_config/request_referrer_policy.dart b/lib/models/web_config/request_referrer_policy.dart
new file mode 100644
index 0000000..8220460
--- /dev/null
+++ b/lib/models/web_config/request_referrer_policy.dart
@@ -0,0 +1,35 @@
+// This code is adapted from fetch_client package
+// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
+// Licensed under the MIT License
+
+import 'package:fetch_client/fetch_client.dart';
+
+/// Specifies the referrer policy to use for the request.
+enum WebConfigRequestReferrerPolicy {
+ strictOriginWhenCrossOrigin,
+ noReferrer,
+ noReferrerWhenDowngrade,
+ sameOrigin,
+ origin,
+ strictOrigin,
+ originWhenCrossOrigin,
+ unsafeUrl;
+
+ RequestReferrerPolicy toRequestReferrerPolicy() => switch (this) {
+ WebConfigRequestReferrerPolicy.strictOriginWhenCrossOrigin =>
+ RequestReferrerPolicy.strictOriginWhenCrossOrigin,
+ WebConfigRequestReferrerPolicy.noReferrer =>
+ RequestReferrerPolicy.noReferrer,
+ WebConfigRequestReferrerPolicy.noReferrerWhenDowngrade =>
+ RequestReferrerPolicy.noReferrerWhenDowngrade,
+ WebConfigRequestReferrerPolicy.sameOrigin =>
+ RequestReferrerPolicy.sameOrigin,
+ WebConfigRequestReferrerPolicy.origin => RequestReferrerPolicy.origin,
+ WebConfigRequestReferrerPolicy.strictOrigin =>
+ RequestReferrerPolicy.strictOrigin,
+ WebConfigRequestReferrerPolicy.originWhenCrossOrigin =>
+ RequestReferrerPolicy.originWhenCrossOrigin,
+ WebConfigRequestReferrerPolicy.unsafeUrl =>
+ RequestReferrerPolicy.unsafeUrl,
+ };
+}
diff --git a/lib/models/web_config/web_config.dart b/lib/models/web_config/web_config.dart
new file mode 100644
index 0000000..c66fd28
--- /dev/null
+++ b/lib/models/web_config/web_config.dart
@@ -0,0 +1,95 @@
+// This code is adapted from fetch_client package
+// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors
+// Licensed under the MIT License
+
+import 'redirect_policy.dart';
+import 'request_cache.dart';
+import 'request_credentials.dart';
+import 'request_mode.dart';
+import 'request_referrer_policy.dart';
+
+/// Configuration for web requests.
+class WebConfig {
+ /// Create a new web configuration.
+ WebConfig({
+ this.mode = WebConfigRequestMode.noCors,
+ this.credentials = WebConfigRequestCredentials.sameOrigin,
+ this.cache = WebConfigRequestCache.byDefault,
+ this.referrer = '',
+ this.referrerPolicy =
+ WebConfigRequestReferrerPolicy.strictOriginWhenCrossOrigin,
+ this.redirectPolicy = WebConfigRedirectPolicy.alwaysFollow,
+ this.streamRequests = false,
+ });
+
+ /// The request mode.
+ final WebConfigRequestMode mode;
+
+ /// The credentials mode, defines what browsers do with credentials.
+ final WebConfigRequestCredentials credentials;
+
+ /// The cache mode which controls how requests will interact with
+ /// the browser's HTTP cache.
+ final WebConfigRequestCache cache;
+
+ /// The referrer.
+ /// This can be a same-origin URL, `about:client`, or an empty string.
+ final String referrer;
+
+ /// The referrer policy.
+ final WebConfigRequestReferrerPolicy referrerPolicy;
+
+ /// The redirect policy, defines how client should handle redirects.
+ final WebConfigRedirectPolicy redirectPolicy;
+
+ /// Whether to use streaming for requests.
+ ///
+ /// **NOTICE**: This feature is supported only in __Chromium 105+__ based browsers
+ /// and requires server to be HTTP/2 or HTTP/3.
+ final bool streamRequests;
+
+ /// Creates a copy of this configuration with the given fields replaced with the new values.
+ WebConfig copyWith({
+ WebConfigRequestMode? mode,
+ WebConfigRequestCredentials? credentials,
+ WebConfigRequestCache? cache,
+ String? referrer,
+ WebConfigRequestReferrerPolicy? referrerPolicy,
+ WebConfigRedirectPolicy? redirectPolicy,
+ bool? streamRequests,
+ }) {
+ return WebConfig(
+ mode: mode ?? this.mode,
+ credentials: credentials ?? this.credentials,
+ cache: cache ?? this.cache,
+ referrer: referrer ?? this.referrer,
+ referrerPolicy: referrerPolicy ?? this.referrerPolicy,
+ redirectPolicy: redirectPolicy ?? this.redirectPolicy,
+ streamRequests: streamRequests ?? this.streamRequests,
+ );
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+ return other is WebConfig &&
+ other.mode == mode &&
+ other.credentials == credentials &&
+ other.cache == cache &&
+ other.referrer == referrer &&
+ other.referrerPolicy == referrerPolicy &&
+ other.redirectPolicy == redirectPolicy &&
+ other.streamRequests == streamRequests;
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ mode,
+ credentials,
+ cache,
+ referrer,
+ referrerPolicy,
+ redirectPolicy,
+ streamRequests,
+ );
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 77643d1..3f80d0e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: eventflux
description: "Efficient handling of server-sent event streams with easy connectivity and data management."
-version: 2.2.2-dev.2
+version: 2.3.0-dev.1
homepage: https://gokula.dev
repository: https://github.com/Imgkl/EventFlux
issue_tracker: https://github.com/Imgkl/EventFlux/issues
@@ -14,6 +14,7 @@ environment:
flutter: ">=1.17.0"
dependencies:
+ fetch_client: ^1.1.2
flutter:
sdk: flutter
http: ^1.2.2
diff --git a/test/browser/client_browser_test.dart b/test/browser/client_browser_test.dart
new file mode 100644
index 0000000..b1180dd
--- /dev/null
+++ b/test/browser/client_browser_test.dart
@@ -0,0 +1,119 @@
+@TestOn('browser')
+import 'dart:async';
+
+import 'package:eventflux/eventflux.dart';
+import 'package:eventflux/models/web_config/redirect_policy.dart';
+import 'package:eventflux/models/web_config/request_cache.dart';
+import 'package:eventflux/models/web_config/request_credentials.dart';
+import 'package:eventflux/models/web_config/request_mode.dart';
+import 'package:eventflux/models/web_config/request_referrer_policy.dart';
+import 'package:eventflux/models/web_config/web_config.dart';
+import 'package:fake_async/fake_async.dart';
+import 'package:fetch_client/fetch_client.dart';
+import 'package:test/test.dart';
+
+/// Run this with `flutter test --platform chrome test/browser/eventflux_test_browser.dart`
+void main() {
+ late EventFlux eventFlux;
+
+ setUp(() {
+ eventFlux = EventFlux.spawn();
+ });
+
+ for (final connectionType in [
+ EventFluxConnectionType.get,
+ EventFluxConnectionType.post,
+ ]) {
+ test(
+ 'uses FetchClient in browser environment with correct config for $connectionType',
+ () {
+ final events = [];
+ final completer = Completer();
+ const testUrl = 'https://localhost:4567';
+
+ fakeAsync((async) {
+ eventFlux.connect(
+ connectionType,
+ testUrl,
+ webConfig: WebConfig(
+ mode: WebConfigRequestMode.sameOrigin,
+ credentials: WebConfigRequestCredentials.cors,
+ cache: WebConfigRequestCache.noCache,
+ referrer: 'https://localhost:4567',
+ referrerPolicy: WebConfigRequestReferrerPolicy.noReferrer,
+ redirectPolicy: WebConfigRedirectPolicy.probe,
+ streamRequests: true,
+ ),
+ onSuccessCallback: (response) {
+ response?.stream?.listen(
+ (event) {
+ events.add(event.data);
+ if (events.length >= 3) {
+ completer.complete();
+ }
+ },
+ onError: (error) => completer.completeError(error),
+ );
+ },
+ onError: (error) => completer.completeError(error),
+ );
+ });
+
+ expect(
+ eventFlux.client,
+ isA()
+ .having(
+ (c) => c.mode,
+ 'mode',
+ RequestMode.sameOrigin,
+ )
+ .having(
+ (c) => c.credentials,
+ 'credentials',
+ RequestCredentials.cors,
+ )
+ .having(
+ (c) => c.cache,
+ 'cache',
+ RequestCache.noCache,
+ )
+ .having(
+ (c) => c.referrer,
+ 'referrer',
+ 'https://localhost:4567',
+ )
+ .having(
+ (c) => c.referrerPolicy,
+ 'referrerPolicy',
+ RequestReferrerPolicy.noReferrer,
+ )
+ .having(
+ (c) => c.redirectPolicy,
+ 'redirectPolicy',
+ RedirectPolicy.probe,
+ )
+ .having(
+ (c) => c.streamRequests,
+ 'streamRequests',
+ true,
+ ),
+ );
+ },
+ timeout: const Timeout(Duration(seconds: 10)),
+ );
+ }
+
+ test('throws Assertion error if webConfig is not provided', () {
+ expect(
+ () => fakeAsync((async) {
+ eventFlux.connect(
+ EventFluxConnectionType.get,
+ 'https://localhost:4567',
+ onSuccessCallback: (_) {},
+ onError: (_) {},
+ );
+ }),
+ throwsA(isA()),
+ );
+ });
+}
diff --git a/test/browser/fetch_client_extension_test.dart b/test/browser/fetch_client_extension_test.dart
new file mode 100644
index 0000000..5b1dded
--- /dev/null
+++ b/test/browser/fetch_client_extension_test.dart
@@ -0,0 +1,115 @@
+@TestOn('browser')
+import 'package:eventflux/extensions/fetch_client_extension.dart';
+import 'package:eventflux/models/web_config/redirect_policy.dart';
+import 'package:eventflux/models/web_config/request_cache.dart';
+import 'package:eventflux/models/web_config/request_credentials.dart';
+import 'package:eventflux/models/web_config/request_mode.dart';
+import 'package:eventflux/models/web_config/request_referrer_policy.dart';
+import 'package:eventflux/models/web_config/web_config.dart';
+import 'package:fetch_client/fetch_client.dart';
+import 'package:test/test.dart';
+
+/// Run this with `flutter test test/browser/fetch_client_extensions_test.dart`
+void main() {
+ group('FetchClientExtension', () {
+ test('fromWebConfig creates FetchClient with correct parameters', () {
+ final webConfig = WebConfig(
+ mode: WebConfigRequestMode.sameOrigin,
+ credentials: WebConfigRequestCredentials.cors,
+ cache: WebConfigRequestCache.noCache,
+ referrer: 'https://test.com',
+ referrerPolicy: WebConfigRequestReferrerPolicy.noReferrer,
+ redirectPolicy: WebConfigRedirectPolicy.probe,
+ streamRequests: true,
+ );
+
+ final result = FetchClientExtension.fromWebConfig(webConfig);
+
+ expect(
+ result,
+ isA()
+ .having(
+ (c) => c.mode,
+ 'mode',
+ RequestMode.sameOrigin,
+ )
+ .having(
+ (c) => c.credentials,
+ 'credentials',
+ RequestCredentials.cors,
+ )
+ .having(
+ (c) => c.cache,
+ 'cache',
+ RequestCache.noCache,
+ )
+ .having(
+ (c) => c.referrer,
+ 'referrer',
+ 'https://test.com',
+ )
+ .having(
+ (c) => c.referrerPolicy,
+ 'referrerPolicy',
+ RequestReferrerPolicy.noReferrer,
+ )
+ .having(
+ (c) => c.redirectPolicy,
+ 'redirectPolicy',
+ RedirectPolicy.probe,
+ )
+ .having(
+ (c) => c.streamRequests,
+ 'streamRequests',
+ true,
+ ),
+ );
+ });
+
+ test('fromWebConfig creates FetchClient with default WebConfig values', () {
+ final webConfig = WebConfig();
+
+ final result = FetchClientExtension.fromWebConfig(webConfig);
+
+ expect(
+ result,
+ isA()
+ .having(
+ (c) => c.mode,
+ 'mode',
+ RequestMode.noCors,
+ )
+ .having(
+ (c) => c.credentials,
+ 'credentials',
+ RequestCredentials.sameOrigin,
+ )
+ .having(
+ (c) => c.cache,
+ 'cache',
+ RequestCache.byDefault,
+ )
+ .having(
+ (c) => c.referrer,
+ 'referrer',
+ '',
+ )
+ .having(
+ (c) => c.referrerPolicy,
+ 'referrerPolicy',
+ RequestReferrerPolicy.strictOriginWhenCrossOrigin,
+ )
+ .having(
+ (c) => c.redirectPolicy,
+ 'redirectPolicy',
+ RedirectPolicy.alwaysFollow,
+ )
+ .having(
+ (c) => c.streamRequests,
+ 'streamRequests',
+ false,
+ ),
+ );
+ });
+ });
+}
diff --git a/test/client_vm_test.dart b/test/client_vm_test.dart
new file mode 100644
index 0000000..ab20474
--- /dev/null
+++ b/test/client_vm_test.dart
@@ -0,0 +1,52 @@
+@TestOn('vm')
+import 'dart:async';
+
+import 'package:eventflux/eventflux.dart';
+import 'package:fake_async/fake_async.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+/// Run this with `flutter test test/eventflux_test_vm.dart`
+void main() {
+ late EventFlux eventFlux;
+
+ setUp(() {
+ eventFlux = EventFlux.spawn();
+ });
+
+ for (final connectionType in [
+ EventFluxConnectionType.get,
+ EventFluxConnectionType.post,
+ ]) {
+ test(
+ 'uses Client in non-browser environment for $connectionType',
+ () {
+ final events = [];
+ final completer = Completer();
+ const testUrl = 'https://localhost:4567';
+
+ fakeAsync((async) {
+ eventFlux.connect(
+ connectionType,
+ testUrl,
+ onSuccessCallback: (response) {
+ response?.stream?.listen(
+ (event) {
+ events.add(event.data);
+ if (events.length >= 3) {
+ completer.complete();
+ }
+ },
+ onError: (error) => completer.completeError(error),
+ );
+ },
+ onError: (error) => completer.completeError(error),
+ );
+ });
+
+ expect(eventFlux.client, isA());
+ },
+ timeout: const Timeout(Duration(seconds: 10)),
+ );
+ }
+}
diff --git a/test/models/web_config/redirect_policy_test.dart b/test/models/web_config/redirect_policy_test.dart
new file mode 100644
index 0000000..74907d1
--- /dev/null
+++ b/test/models/web_config/redirect_policy_test.dart
@@ -0,0 +1,28 @@
+import 'package:eventflux/models/web_config/redirect_policy.dart';
+import 'package:fetch_client/fetch_client.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('WebConfigRedirectPolicy.toRedirectPolicy', () {
+ test('alwaysFollow converts to RedirectPolicy.alwaysFollow', () {
+ expect(
+ WebConfigRedirectPolicy.alwaysFollow.toRedirectPolicy(),
+ RedirectPolicy.alwaysFollow,
+ );
+ });
+
+ test('probe converts to RedirectPolicy.probe', () {
+ expect(
+ WebConfigRedirectPolicy.probe.toRedirectPolicy(),
+ RedirectPolicy.probe,
+ );
+ });
+
+ test('probeHead converts to RedirectPolicy.probeHead', () {
+ expect(
+ WebConfigRedirectPolicy.probeHead.toRedirectPolicy(),
+ RedirectPolicy.probeHead,
+ );
+ });
+ });
+}
diff --git a/test/models/web_config/request_cache_test.dart b/test/models/web_config/request_cache_test.dart
new file mode 100644
index 0000000..44006c8
--- /dev/null
+++ b/test/models/web_config/request_cache_test.dart
@@ -0,0 +1,49 @@
+import 'package:eventflux/models/web_config/request_cache.dart';
+import 'package:fetch_client/fetch_client.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('WebConfigRequestCache.toRequestCache', () {
+ test('byDefault converts to RequestCache.byDefault', () {
+ expect(
+ WebConfigRequestCache.byDefault.toRequestCache(),
+ RequestCache.byDefault,
+ );
+ });
+
+ test('noStore converts to RequestCache.noStore', () {
+ expect(
+ WebConfigRequestCache.noStore.toRequestCache(),
+ RequestCache.noStore,
+ );
+ });
+
+ test('reload converts to RequestCache.reload', () {
+ expect(
+ WebConfigRequestCache.reload.toRequestCache(),
+ RequestCache.reload,
+ );
+ });
+
+ test('noCache converts to RequestCache.noCache', () {
+ expect(
+ WebConfigRequestCache.noCache.toRequestCache(),
+ RequestCache.noCache,
+ );
+ });
+
+ test('forceCache converts to RequestCache.forceCache', () {
+ expect(
+ WebConfigRequestCache.forceCache.toRequestCache(),
+ RequestCache.forceCache,
+ );
+ });
+
+ test('onlyIfCached converts to RequestCache.onlyIfCached', () {
+ expect(
+ WebConfigRequestCache.onlyIfCached.toRequestCache(),
+ RequestCache.onlyIfCached,
+ );
+ });
+ });
+}
diff --git a/test/models/web_config/request_credentials_test.dart b/test/models/web_config/request_credentials_test.dart
new file mode 100644
index 0000000..4592b70
--- /dev/null
+++ b/test/models/web_config/request_credentials_test.dart
@@ -0,0 +1,28 @@
+import 'package:eventflux/models/web_config/request_credentials.dart';
+import 'package:fetch_client/fetch_client.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('WebConfigRequestCredentials.toRequestCredentials', () {
+ test('sameOrigin converts to RequestCredentials.sameOrigin', () {
+ expect(
+ WebConfigRequestCredentials.sameOrigin.toRequestCredentials(),
+ RequestCredentials.sameOrigin,
+ );
+ });
+
+ test('omit converts to RequestCredentials.omit', () {
+ expect(
+ WebConfigRequestCredentials.omit.toRequestCredentials(),
+ RequestCredentials.omit,
+ );
+ });
+
+ test('cors converts to RequestCredentials.cors', () {
+ expect(
+ WebConfigRequestCredentials.cors.toRequestCredentials(),
+ RequestCredentials.cors,
+ );
+ });
+ });
+}
diff --git a/test/models/web_config/request_mode_test.dart b/test/models/web_config/request_mode_test.dart
new file mode 100644
index 0000000..068b952
--- /dev/null
+++ b/test/models/web_config/request_mode_test.dart
@@ -0,0 +1,42 @@
+import 'package:eventflux/models/web_config/request_mode.dart';
+import 'package:fetch_client/fetch_client.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('WebConfigRequestMode.toRequestMode', () {
+ test('sameOrigin converts to RequestMode.sameOrigin', () {
+ expect(
+ WebConfigRequestMode.sameOrigin.toRequestMode(),
+ RequestMode.sameOrigin,
+ );
+ });
+
+ test('noCors converts to RequestMode.noCors', () {
+ expect(
+ WebConfigRequestMode.noCors.toRequestMode(),
+ RequestMode.noCors,
+ );
+ });
+
+ test('cors converts to RequestMode.cors', () {
+ expect(
+ WebConfigRequestMode.cors.toRequestMode(),
+ RequestMode.cors,
+ );
+ });
+
+ test('navigate converts to RequestMode.navigate', () {
+ expect(
+ WebConfigRequestMode.navigate.toRequestMode(),
+ RequestMode.navigate,
+ );
+ });
+
+ test('webSocket converts to RequestMode.webSocket', () {
+ expect(
+ WebConfigRequestMode.webSocket.toRequestMode(),
+ RequestMode.webSocket,
+ );
+ });
+ });
+}
diff --git a/test/models/web_config/request_referrer_policy_test.dart b/test/models/web_config/request_referrer_policy_test.dart
new file mode 100644
index 0000000..39364c6
--- /dev/null
+++ b/test/models/web_config/request_referrer_policy_test.dart
@@ -0,0 +1,66 @@
+import 'package:eventflux/models/web_config/request_referrer_policy.dart';
+import 'package:fetch_client/fetch_client.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('WebConfigRequestReferrerPolicy.toRequestReferrerPolicy', () {
+ test('strictOriginWhenCrossOrigin converts correctly', () {
+ expect(
+ WebConfigRequestReferrerPolicy.strictOriginWhenCrossOrigin
+ .toRequestReferrerPolicy(),
+ RequestReferrerPolicy.strictOriginWhenCrossOrigin,
+ );
+ });
+
+ test('noReferrer converts correctly', () {
+ expect(
+ WebConfigRequestReferrerPolicy.noReferrer.toRequestReferrerPolicy(),
+ RequestReferrerPolicy.noReferrer,
+ );
+ });
+
+ test('noReferrerWhenDowngrade converts correctly', () {
+ expect(
+ WebConfigRequestReferrerPolicy.noReferrerWhenDowngrade
+ .toRequestReferrerPolicy(),
+ RequestReferrerPolicy.noReferrerWhenDowngrade,
+ );
+ });
+
+ test('sameOrigin converts correctly', () {
+ expect(
+ WebConfigRequestReferrerPolicy.sameOrigin.toRequestReferrerPolicy(),
+ RequestReferrerPolicy.sameOrigin,
+ );
+ });
+
+ test('origin converts correctly', () {
+ expect(
+ WebConfigRequestReferrerPolicy.origin.toRequestReferrerPolicy(),
+ RequestReferrerPolicy.origin,
+ );
+ });
+
+ test('strictOrigin converts correctly', () {
+ expect(
+ WebConfigRequestReferrerPolicy.strictOrigin.toRequestReferrerPolicy(),
+ RequestReferrerPolicy.strictOrigin,
+ );
+ });
+
+ test('originWhenCrossOrigin converts correctly', () {
+ expect(
+ WebConfigRequestReferrerPolicy.originWhenCrossOrigin
+ .toRequestReferrerPolicy(),
+ RequestReferrerPolicy.originWhenCrossOrigin,
+ );
+ });
+
+ test('unsafeUrl converts correctly', () {
+ expect(
+ WebConfigRequestReferrerPolicy.unsafeUrl.toRequestReferrerPolicy(),
+ RequestReferrerPolicy.unsafeUrl,
+ );
+ });
+ });
+}
diff --git a/test/models/web_config/web_config_test.dart b/test/models/web_config/web_config_test.dart
new file mode 100644
index 0000000..7de2404
--- /dev/null
+++ b/test/models/web_config/web_config_test.dart
@@ -0,0 +1,75 @@
+import 'package:eventflux/models/web_config/redirect_policy.dart';
+import 'package:eventflux/models/web_config/request_cache.dart';
+import 'package:eventflux/models/web_config/request_credentials.dart';
+import 'package:eventflux/models/web_config/request_mode.dart';
+import 'package:eventflux/models/web_config/request_referrer_policy.dart';
+import 'package:eventflux/models/web_config/web_config.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('WebConfig', () {
+ test('constructor has correct default values', () {
+ expect(
+ WebConfig(),
+ WebConfig(
+ mode: WebConfigRequestMode.noCors,
+ credentials: WebConfigRequestCredentials.sameOrigin,
+ cache: WebConfigRequestCache.byDefault,
+ referrer: '',
+ referrerPolicy:
+ WebConfigRequestReferrerPolicy.strictOriginWhenCrossOrigin,
+ redirectPolicy: WebConfigRedirectPolicy.alwaysFollow,
+ streamRequests: false,
+ ),
+ );
+ });
+
+ group('copyWith', () {
+ test('returns a copy of itself', () {
+ final webConfig = WebConfig();
+ final copy = webConfig.copyWith();
+ expect(copy, webConfig);
+ });
+
+ test('returns a copy with the given fields replaced', () {
+ final webConfig = WebConfig();
+ final copy = webConfig.copyWith(
+ mode: WebConfigRequestMode.sameOrigin,
+ credentials: WebConfigRequestCredentials.cors,
+ );
+ expect(
+ copy,
+ WebConfig(
+ mode: WebConfigRequestMode.sameOrigin,
+ credentials: WebConfigRequestCredentials.cors,
+ ),
+ );
+ });
+
+ test('returns a copy with all fields replaced', () {
+ final webConfig = WebConfig();
+ final copy = webConfig.copyWith(
+ mode: WebConfigRequestMode.sameOrigin,
+ credentials: WebConfigRequestCredentials.cors,
+ cache: WebConfigRequestCache.noCache,
+ referrer: 'https://test.com',
+ referrerPolicy: WebConfigRequestReferrerPolicy.noReferrer,
+ redirectPolicy: WebConfigRedirectPolicy.probe,
+ streamRequests: true,
+ );
+ expect(
+ copy,
+ WebConfig(
+ mode: WebConfigRequestMode.sameOrigin,
+ credentials: WebConfigRequestCredentials.cors,
+ cache: WebConfigRequestCache.noCache,
+ referrer: 'https://test.com',
+ referrerPolicy: WebConfigRequestReferrerPolicy.noReferrer,
+ redirectPolicy: WebConfigRedirectPolicy.probe,
+ streamRequests: true,
+ ),
+ );
+ });
+ });
+ });
+}