Add a test for Class inspection (#2310)

diff --git a/dwds/test/instances/class_inspection_test.dart b/dwds/test/instances/class_inspection_test.dart
new file mode 100644
index 0000000..aa05e6d
--- /dev/null
+++ b/dwds/test/instances/class_inspection_test.dart
@@ -0,0 +1,127 @@
+// Copyright (c) 2023, 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.
+
+@Tags(['daily'])
+@TestOn('vm')
+@Timeout(Duration(minutes: 2))
+
+import 'package:test/test.dart';
+import 'package:test_common/logging.dart';
+import 'package:test_common/test_sdk_configuration.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../fixtures/context.dart';
+import '../fixtures/project.dart';
+import '../fixtures/utilities.dart';
+import 'common/test_inspector.dart';
+
+void main() {
+  // Enable verbose logging for debugging.
+  final debug = false;
+
+  final provider = TestSdkConfigurationProvider(
+    verbose: debug,
+  );
+
+  final context =
+      TestContext(TestProject.testExperimentWithSoundNullSafety, provider);
+  final testInspector = TestInspector(context);
+
+  late VmService service;
+  late Stream<Event> stream;
+  late String isolateId;
+  late ScriptRef mainScript;
+
+  onBreakPoint(breakPointId, body) => testInspector.onBreakPoint(
+        stream,
+        isolateId,
+        mainScript,
+        breakPointId,
+        body,
+      );
+
+  getObject(instanceId) => service.getObject(isolateId, instanceId);
+
+  group('Class |', () {
+    tearDownAll(provider.dispose);
+
+    for (var compilationMode in CompilationMode.values) {
+      group('$compilationMode |', () {
+        setUpAll(() async {
+          setCurrentLogWriter(debug: debug);
+          await context.setUp(
+            testSettings: TestSettings(
+              compilationMode: compilationMode,
+              enableExpressionEvaluation: true,
+              verboseCompiler: debug,
+            ),
+          );
+          service = context.debugConnection.vmService;
+
+          final vm = await service.getVM();
+          isolateId = vm.isolates!.first.id!;
+          final scripts = await service.getScripts(isolateId);
+
+          await service.streamListen('Debug');
+          stream = service.onEvent('Debug');
+
+          mainScript = scripts.scripts!
+              .firstWhere((each) => each.uri!.contains('main.dart'));
+        });
+
+        tearDownAll(() async {
+          await context.tearDown();
+        });
+
+        setUp(() => setCurrentLogWriter(debug: debug));
+        tearDown(() => service.resume(isolateId));
+
+        group('calling getObject for an existent class', () {
+          test('returns the correct class representation', () async {
+            await onBreakPoint('testClass1Case1', (event) async {
+              // classes|dart:core|Object_Diagnosticable
+              final result = await getObject(
+                'classes|org-dartlang-app:///web/main.dart|GreeterClass',
+              );
+              final clazz = result as Class?;
+              expect(clazz!.name, equals('GreeterClass'));
+              expect(
+                clazz.fields!.map((field) => field.name),
+                unorderedEquals([
+                  'greeteeName',
+                  'useFrench',
+                ]),
+              );
+              expect(
+                clazz.functions!.map((fn) => fn.name),
+                containsAll([
+                  'sayHello',
+                  'greetInEnglish',
+                  'greetInFrench',
+                ]),
+              );
+            });
+          });
+        });
+
+        group('calling getObject for a non-existent class', () {
+          // TODO(https://github.com/dart-lang/webdev/issues/2297): Ideally we
+          // should throw an error in this case for the client to catch instead
+          // of returning an empty class.
+          test('returns an empty class representation', () async {
+            await onBreakPoint('testClass1Case1', (event) async {
+              final result = await getObject(
+                'classes|dart:core|Object_Diagnosticable',
+              );
+              final clazz = result as Class?;
+              expect(clazz!.name, equals('Object_Diagnosticable'));
+              expect(clazz.fields, isEmpty);
+              expect(clazz.functions, isEmpty);
+            });
+          });
+        });
+      });
+    }
+  });
+}
diff --git a/fixtures/_experimentSound/web/main.dart b/fixtures/_experimentSound/web/main.dart
index 3783bc6..b68f1d4 100644
--- a/fixtures/_experimentSound/web/main.dart
+++ b/fixtures/_experimentSound/web/main.dart
@@ -20,6 +20,8 @@
     testPattern([3.14, 'b']);
     testPattern([0, 1]);
     testPattern2();
+    print('Classes');
+    testClass();
   });
 
   document.body!.appendText('Program is running!');
@@ -55,6 +57,11 @@
   print(record); // Breakpoint: printNestedNamedLocalRecord
 }
 
+void testClass() {
+  final greeter = GreeterClass(greeteeName: 'Charlie Brown');
+  greeter.sayHello();
+}
+
 String testPattern(Object obj) {
   switch (obj) {
     case [var a, int n] || [int n, var a] when n == 1 && a is String:
@@ -73,3 +80,25 @@
   print(firstCat); // Breakpoint: testPattern2Case2
   return '$dog, $firstCat, $secondCat';
 }
+
+class GreeterClass {
+  final String greeteeName;
+  final bool useFrench;
+
+  GreeterClass({
+    this.greeteeName = 'Snoopy',
+    this.useFrench = false,
+  });
+
+  void sayHello() {
+    useFrench ? greetInFrench() : greetInEnglish();
+  }
+
+  void greetInEnglish() {
+    print('Hello $greeteeName'); // Breakpoint: testClass1Case1
+  }
+
+  void greetInFrench() {
+    print('Bonjour $greeteeName');
+  }
+}