diff --git a/lib/web_ui/lib/src/engine/browser_location.dart b/lib/web_ui/lib/src/engine/browser_location.dart index 30bcfcdf64c..854bf9ec6f5 100644 --- a/lib/web_ui/lib/src/engine/browser_location.dart +++ b/lib/web_ui/lib/src/engine/browser_location.dart @@ -73,10 +73,13 @@ class HashLocationStrategy extends LocationStrategy { // and if it is empty then it will stay empty String path = _platformLocation.hash ?? ''; assert(path.isEmpty || path.startsWith('#')); - // Dart will complain if a call to substring is - // executed with a position value that exceeds the - // length of string. - return path.isEmpty ? path : path.substring(1); + + // We don't want to return an empty string as a path. Instead we default to "/". + if (path.isEmpty || path == '#') { + return '/'; + } + // At this point, we know [path] starts with "#" and isn't empty. + return path.substring(1); } @override diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart index 494b565ddd4..58d536aff47 100644 --- a/lib/web_ui/test/engine/history_test.dart +++ b/lib/web_ui/test/engine/history_test.dart @@ -7,6 +7,7 @@ // TODO(nurhan): https://github.com/flutter/flutter/issues/51169 import 'dart:async'; +import 'dart:html' as html; import 'dart:typed_data'; import 'package:test/test.dart'; @@ -28,7 +29,7 @@ const MethodCodec codec = JSONMethodCodec(); void emptyCallback(ByteData date) {} void main() { - group('BrowserHistory', () { + group('$BrowserHistory', () { final PlatformMessagesSpy spy = PlatformMessagesSpy(); setUp(() { @@ -229,6 +230,41 @@ void main() { // TODO(nurhan): https://github.com/flutter/flutter/issues/50836 skip: browserEngine == BrowserEngine.edge); }); + + group('$HashLocationStrategy', () { + TestPlatformLocation location; + + setUp(() { + location = TestPlatformLocation(); + }); + + tearDown(() { + location = null; + }); + + test('leading slash is optional', () { + final HashLocationStrategy strategy = HashLocationStrategy(location); + + location.hash = '#/'; + expect(strategy.path, '/'); + + location.hash = '#/foo'; + expect(strategy.path, '/foo'); + + location.hash = '#foo'; + expect(strategy.path, 'foo'); + }); + + test('path should not be empty', () { + final HashLocationStrategy strategy = HashLocationStrategy(location); + + location.hash = ''; + expect(strategy.path, '/'); + + location.hash = '#'; + expect(strategy.path, '/'); + }); + }); } void pushRoute(String routeName) { @@ -276,3 +312,38 @@ Future systemNavigatorPop() { ); return completer.future; } + +/// A mock implementation of [PlatformLocation] that doesn't access the browser. +class TestPlatformLocation extends PlatformLocation { + String pathname; + String search; + String hash; + + void onPopState(html.EventListener fn) { + throw UnimplementedError(); + } + + void offPopState(html.EventListener fn) { + throw UnimplementedError(); + } + + void onHashChange(html.EventListener fn) { + throw UnimplementedError(); + } + + void offHashChange(html.EventListener fn) { + throw UnimplementedError(); + } + + void pushState(dynamic state, String title, String url) { + throw UnimplementedError(); + } + + void replaceState(dynamic state, String title, String url) { + throw UnimplementedError(); + } + + void back() { + throw UnimplementedError(); + } +}