Skip to content

Commit 24ff83a

Browse files
authored
fix(web): properly cast Map from JSAny?.dartify() (#18855)
* fix(web): properly cast Map from JSAny?.dartify() and remove dead interop code Fixes a runtime TypeError in firebase_performance_web's Trace.getAttributes() where dartify() on JS objects was incorrectly cast to Map<String, String> instead of Map<Object?, Object?>. Also removes unused and broken dartify() casting helpers in firebase_auth_web and firebase_remote_config_web. * test(performance,web): add license header and @teston('browser') * fix(remote_config,web): restore and fix RemoteConfig.defaultConfig getter * fix(auth,web): restore User.toJson() with Map.cast and add browser unit tests
1 parent f3d7cf6 commit 24ff83a

6 files changed

Lines changed: 130 additions & 6 deletions

File tree

‎packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@ class User extends UserInfo<auth_interop.UserJsImpl> {
268268

269269
/// Returns a JSON-serializable representation of this object.
270270
Map<String, dynamic> toJson() {
271-
final result = jsObject.toJSON();
272-
return (result as JSAny).dartify()! as Map<String, dynamic>;
271+
return (jsObject.toJSON().dartify() as Map?)?.cast<String, dynamic>() ??
272+
<String, dynamic>{};
273273
}
274274

275275
@override
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2026 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@TestOn('browser')
6+
library;
7+
8+
import 'dart:js_interop';
9+
import 'dart:js_interop_unsafe';
10+
11+
import 'package:firebase_auth_web/src/interop/auth.dart' as auth_interop;
12+
import 'package:firebase_auth_web/src/interop/auth_interop.dart'
13+
as auth_js_interop;
14+
import 'package:flutter_test/flutter_test.dart';
15+
16+
void main() {
17+
group('User.toJson', () {
18+
test('converts JSObject from toJSON() to Map<String, dynamic>', () {
19+
final jsJson = JSObject()
20+
..['uid'] = 'user-123'.toJS
21+
..['email'] = 'user@example.com'.toJS
22+
..['emailVerified'] = true.toJS;
23+
final jsUser = JSObject()..['toJSON'] = (() => jsJson).toJS;
24+
25+
final user = auth_interop.User.getInstance(
26+
jsUser as auth_js_interop.UserJsImpl,
27+
)!;
28+
final json = user.toJson();
29+
30+
expect(json, isA<Map<String, dynamic>>());
31+
expect(
32+
json,
33+
equals(<String, dynamic>{
34+
'uid': 'user-123',
35+
'email': 'user@example.com',
36+
'emailVerified': true,
37+
}),
38+
);
39+
});
40+
41+
test('handles an empty JSObject from toJSON()', () {
42+
final emptyJson = JSObject();
43+
final jsUser = JSObject()..['toJSON'] = (() => emptyJson).toJS;
44+
45+
final user = auth_interop.User.getInstance(
46+
jsUser as auth_js_interop.UserJsImpl,
47+
)!;
48+
final json = user.toJson();
49+
50+
expect(json, isA<Map<String, dynamic>>());
51+
expect(json, isEmpty);
52+
});
53+
});
54+
}

‎packages/firebase_performance/firebase_performance_web/lib/src/interop/performance.dart‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ class Trace extends JsObjectWrapper<performance_interop.TraceJsImpl> {
5656
String getAttribute(String attr) => jsObject.getAttribute(attr.toJS).toDart;
5757

5858
Map<String, String> getAttributes() {
59-
return (jsObject.getAttributes().dartify()! as Map<String, String>)
60-
.cast<String, String>();
59+
return (jsObject.getAttributes().dartify() as Map?)
60+
?.cast<String, String>() ??
61+
<String, String>{};
6162
}
6263

6364
int getMetric(String metricName) =>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2026 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@TestOn('browser')
6+
library;
7+
8+
import 'dart:js_interop';
9+
import 'dart:js_interop_unsafe';
10+
11+
import 'package:firebase_performance_web/src/interop/performance.dart';
12+
import 'package:firebase_performance_web/src/interop/performance_interop.dart'
13+
as performance_interop;
14+
import 'package:flutter_test/flutter_test.dart';
15+
16+
void main() {
17+
group('Trace', () {
18+
test('getAttributes returns a Map<String, String>', () {
19+
final jsAttributes = JSObject()
20+
..['a'] = 'b'.toJS
21+
..['c'] = 'd'.toJS;
22+
final jsTrace = JSObject()..['getAttributes'] = (() => jsAttributes).toJS;
23+
24+
final trace = Trace.fromJsObject(
25+
jsTrace as performance_interop.TraceJsImpl,
26+
);
27+
final attrs = trace.getAttributes();
28+
29+
expect(attrs, isA<Map<String, String>>());
30+
expect(attrs, equals(<String, String>{'a': 'b', 'c': 'd'}));
31+
});
32+
33+
test('getAttributes handles empty maps safely', () {
34+
final emptyAttributes = JSObject();
35+
final jsTrace = JSObject()
36+
..['getAttributes'] = (() => emptyAttributes).toJS;
37+
38+
final trace = Trace.fromJsObject(
39+
jsTrace as performance_interop.TraceJsImpl,
40+
);
41+
final attrs = trace.getAttributes();
42+
43+
expect(attrs, isA<Map<String, String>>());
44+
expect(attrs, isEmpty);
45+
});
46+
});
47+
}

‎packages/firebase_remote_config/firebase_remote_config_web/lib/src/interop/firebase_remote_config.dart‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ class RemoteConfig
5050
/// defaultsMap['x'] = 1; // remoteConfig.defaultConfig will not be updated.
5151
/// remoteConfig.defaultConfig['x'] = 1; // Runtime error: attempt to modify an unmodifiable map.
5252
/// ```
53-
Map<String, dynamic> get defaultConfig => Map.unmodifiable(
54-
jsObject.defaultConfig.dartify()! as Map<String, dynamic>,
53+
Map<String, dynamic> get defaultConfig => Map<String, dynamic>.unmodifiable(
54+
(jsObject.defaultConfig.dartify() as Map?)?.cast<String, dynamic>() ??
55+
<String, dynamic>{},
5556
);
5657

5758
set defaultConfig(Map<String, dynamic> value) {

‎packages/firebase_remote_config/firebase_remote_config_web/test/firebase_remote_config_web_test.dart‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
// Use of this source code is governed by a BSD-style license that can be
44
// found in the LICENSE file.
55
@TestOn('chrome')
6+
import 'dart:js_interop';
7+
68
import 'package:firebase_remote_config_platform_interface/firebase_remote_config_platform_interface.dart';
79
import 'package:firebase_remote_config_web/firebase_remote_config_web.dart';
10+
import 'package:firebase_remote_config_web/src/interop/firebase_remote_config.dart'
11+
as remote_config_interop_wrapper;
12+
import 'package:firebase_remote_config_web/src/interop/firebase_remote_config_interop.dart'
13+
as remote_config_interop;
814
import 'package:flutter_test/flutter_test.dart';
915
import 'package:mockito/annotations.dart';
1016
import 'package:mockito/mockito.dart';
@@ -109,5 +115,20 @@ void main() {
109115
verify(remoteConfig.setDefaults(parameters));
110116
verifyNoMoreInteractions(remoteConfig);
111117
});
118+
119+
test('RemoteConfig.defaultConfig round-trips and returns unmodifiable Map',
120+
() {
121+
final jsRemoteConfig =
122+
JSObject() as remote_config_interop.RemoteConfigJsImpl;
123+
final wrapper = remote_config_interop_wrapper.RemoteConfig.getInstance(
124+
jsRemoteConfig);
125+
126+
wrapper.defaultConfig = <String, dynamic>{'title': 'Hello', 'counter': 1};
127+
final config = wrapper.defaultConfig;
128+
129+
expect(config, isA<Map<String, dynamic>>());
130+
expect(config, equals(<String, dynamic>{'title': 'Hello', 'counter': 1}));
131+
expect(() => config['x'] = 2, throwsUnsupportedError);
132+
});
112133
});
113134
}

0 commit comments

Comments
 (0)