[package:vm_service] Add dot_shorthands_test

Fixes: https://github.com/dart-lang/sdk/issues/59875
Change-Id: I2653a6473a00cbf89b26872c7e50a34e6d486a22
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/431120
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Derek Xu <derekx@google.com>
diff --git a/pkg/pkg.status b/pkg/pkg.status
index 2486873..61615ce 100644
--- a/pkg/pkg.status
+++ b/pkg/pkg.status
@@ -134,6 +134,7 @@
 vm_service/test/developer_extension_test: SkipByDesign # Debugger is disabled in AOT mode.
 vm_service/test/developer_service_get_isolate_id_test: SkipByDesign # Debugger is disabled in AOT mode.
 vm_service/test/developer_service_get_object_id_test: SkipByDesign # Debugger is disabled in AOT mode.
+vm_service/test/dot_shorthands_test: SkipByDesign # Debugger is disabled in AOT mode.
 vm_service/test/enhanced_enum_test: SkipByDesign # Debugger is disabled in AOT mode.
 vm_service/test/eval_*test: SkipByDesign # Debugger is disabled in AOT mode.
 vm_service/test/evaluate_*test: SkipByDesign # Debugger is disabled in AOT mode.
diff --git a/pkg/vm_service/test/common/service_test_common.dart b/pkg/vm_service/test/common/service_test_common.dart
index fc208ac..465f0e7 100644
--- a/pkg/vm_service/test/common/service_test_common.dart
+++ b/pkg/vm_service/test/common/service_test_common.dart
@@ -212,6 +212,29 @@
   };
 }
 
+extension BreakpointLocation on Breakpoint {
+  Future<(String uri, (int line, int column))> getLocation(
+    VmService service,
+    IsolateRef isolateRef,
+  ) async {
+    if (location?.tokenPos == null) {
+      return ('<unknown>', (-1, -1));
+    }
+
+    final script = (await service.getObject(
+      isolateRef.id!,
+      location!.script!.id!,
+    )) as Script;
+    return (
+      script.uri!,
+      (
+        script.getLineNumberFromTokenPos(location!.tokenPos!) ?? -1,
+        script.getColumnNumberFromTokenPos(location!.tokenPos!) ?? -1
+      )
+    );
+  }
+}
+
 extension FrameLocation on Frame {
   Future<(String uri, (int line, int column))> getLocation(
     VmService service,
diff --git a/pkg/vm_service/test/dot_shorthands_test.dart b/pkg/vm_service/test/dot_shorthands_test.dart
new file mode 100644
index 0000000..9e064c8
--- /dev/null
+++ b/pkg/vm_service/test/dot_shorthands_test.dart
@@ -0,0 +1,138 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// SharedOptions=--enable-experiment=dot-shorthands
+// @dart = 3.9
+
+import 'dart:developer';
+
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+
+import 'common/service_test_common.dart';
+import 'common/test_helper.dart';
+
+const String shortFile = 'dot_shorthands_test.dart';
+
+// AUTOGENERATED START
+//
+// Update these constants by running:
+//
+// dart pkg/vm_service/test/update_line_numbers.dart pkg/vm_service/test/dot_shorthands_test.dart
+//
+const LINE_A = 34;
+const LINE_B = 37;
+const LINE_C = 38;
+const LINE_D = 45;
+const LINE_E = 47;
+const LINE_F = 49;
+// AUTOGENERATED END
+
+class C {
+  int value;
+  C(this.value); // LINE_A
+
+  static C two = C(2);
+  static C get three => C(3); // LINE_B
+  static C four() => C(4); // LINE_C
+}
+
+void testeeMain() {
+  C c = C(1);
+  debugger();
+  // ignore: experiment_not_enabled
+  c = .two; // LINE_D
+  // ignore: experiment_not_enabled
+  c = .three; // LINE_E
+  // ignore: experiment_not_enabled
+  c = .four(); // LINE_F
+  print(c.value);
+}
+
+List<String> stops = [];
+
+const List<String> expected = [
+  '$shortFile:$LINE_D:7', // on '.'
+  '$shortFile:$LINE_E:7', // on '.'
+  '$shortFile:$LINE_B:22', // on '=' of '=>'
+  '$shortFile:$LINE_B:25', // on 'C'
+  '$shortFile:$LINE_A:10', // on 'v' of 'value'
+  '$shortFile:$LINE_A:16', // on ';'
+  '$shortFile:$LINE_B:25', // on 'C'
+  '$shortFile:$LINE_F:7', // on '.'
+  '$shortFile:$LINE_C:16', // on '('
+  '$shortFile:$LINE_C:22', // on 'C'
+  '$shortFile:$LINE_A:10', // on 'v' of 'value'
+  '$shortFile:$LINE_A:16', // on ';'
+  '$shortFile:$LINE_C:22', // on 'C'
+];
+
+final tests = <IsolateTest>[
+  hasStoppedAtBreakpoint,
+
+  // Test interaction of expression evaluation with dot shorthands.
+  (VmService service, IsolateRef isolateRef) async {
+    final isolateId = isolateRef.id!;
+
+    InstanceRef response =
+        await service.evaluateInFrame(isolateId, 0, '(c = .two).value')
+            as InstanceRef;
+    expect(response.valueAsString, '2');
+
+    response =
+        await service.evaluateInFrame(isolateId, 0, '(c = .three).value')
+            as InstanceRef;
+    expect(response.valueAsString, '3');
+
+    response =
+        await service.evaluateInFrame(isolateId, 0, '(c = .four()).value')
+            as InstanceRef;
+    expect(response.valueAsString, '4');
+  },
+
+  // Test interaction of breakpoints with dot shorthands.
+  (VmService service, IsolateRef isolateRef) async {
+    final isolateId = isolateRef.id!;
+    final isolate = await service.getIsolate(isolateId);
+    final lib =
+        (await service.getObject(isolateId, isolate.rootLib!.id!)) as Library;
+    final scriptId = lib.scripts!.first.id!;
+
+    Breakpoint breakpoint = await service.addBreakpoint(
+      isolateId,
+      scriptId,
+      LINE_D,
+    );
+    var (_, (line, column)) = await breakpoint.getLocation(service, isolateRef);
+    expect(breakpoint.enabled, true);
+    expect(line, LINE_D);
+    expect(column, 7); // on '.'
+
+    breakpoint = await service.addBreakpoint(isolateId, scriptId, LINE_E);
+    (_, (line, column)) = await breakpoint.getLocation(service, isolateRef);
+    expect(breakpoint.enabled, true);
+    expect(line, LINE_E);
+    expect(column, 7); // on '.'
+    await service.removeBreakpoint(isolateId, breakpoint.id!);
+
+    breakpoint = await service.addBreakpoint(isolateId, scriptId, LINE_F);
+    (_, (line, column)) = await breakpoint.getLocation(service, isolateRef);
+    expect(breakpoint.enabled, true);
+    expect(line, LINE_F);
+    expect(column, 7); // on '.'
+    await service.removeBreakpoint(isolateId, breakpoint.id!);
+  },
+
+  // Test interaction of single-stepping with dot shorthands.
+  runStepIntoThroughProgramRecordingStops(stops),
+  checkRecordedStops(stops, expected),
+];
+
+void main([args = const <String>[]]) => runIsolateTests(
+  args,
+  tests,
+  'dot_shorthands_test.dart',
+  pauseOnExit: true,
+  testeeConcurrent: testeeMain,
+);