test(web_socket...tests): close connections (#1908)
diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart
index cf39f95..cbaf827 100644
--- a/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart
+++ b/pkgs/web_socket_conformance_tests/lib/src/close_local_tests.dart
@@ -15,6 +15,7 @@
 import 'continuously_writing_server_vm.dart'
     if (dart.library.html) 'continuously_writing_server_web.dart'
     as writing_server;
+import 'utils.dart';
 
 /// Tests that the [WebSocket] can correctly close the connection to the peer.
 void testCloseLocal(
@@ -36,6 +37,7 @@
 
     test('peer writes after close are ignored', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
       await channel.close();
       expect(await channel.events.isEmpty, true);
     });
@@ -57,30 +59,35 @@
 
     test('reserved close code: 1004', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
       await expectLater(
           () => channel.close(1004), throwsA(isA<ArgumentError>()));
     });
 
     test('reserved close code: 2999', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
       await expectLater(
           () => channel.close(2999), throwsA(isA<ArgumentError>()));
     });
 
     test('reserved close code: 5000', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
       await expectLater(
           () => channel.close(5000), throwsA(isA<ArgumentError>()));
     });
 
     test('too long close reason', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
       await expectLater(() => channel.close(3000, 'a'.padLeft(124)),
           throwsA(isA<ArgumentError>()));
     });
 
     test('close', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       await channel.close();
       final closeCode = await httpServerQueue.next as int?;
@@ -93,6 +100,7 @@
 
     test('close with 1000', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       await channel.close(1000);
       final closeCode = await httpServerQueue.next as int?;
@@ -105,6 +113,7 @@
 
     test('with code 3000', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       await channel.close(3000);
       final closeCode = await httpServerQueue.next as int?;
@@ -117,6 +126,7 @@
 
     test('with code 4999', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       await channel.close(4999);
       final closeCode = await httpServerQueue.next as int?;
@@ -129,6 +139,7 @@
 
     test('with code and reason', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       await channel.close(3000, 'Client initiated closure');
       final closeCode = await httpServerQueue.next as int?;
@@ -141,6 +152,7 @@
 
     test('close after close', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       await channel.close(3000, 'Client initiated closure');
 
@@ -151,6 +163,7 @@
 
     test('sendBytes after close', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       await channel.close(3000, 'Client initiated closure');
 
@@ -160,6 +173,7 @@
 
     test('sendText after close', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       await channel.close(3000, 'Client initiated closure');
 
diff --git a/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart
index aca846a..0171e30 100644
--- a/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart
+++ b/pkgs/web_socket_conformance_tests/lib/src/close_remote_tests.dart
@@ -11,6 +11,7 @@
 
 import 'close_remote_server_vm.dart'
     if (dart.library.html) 'close_remote_server_web.dart';
+import 'utils.dart';
 
 /// Tests that the [WebSocket] can correctly receive Close frames from the peer.
 void testCloseRemote(
@@ -32,6 +33,7 @@
 
     test('with code and reason', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       channel.sendText('Please close');
       expect(await channel.events.toList(),
@@ -40,6 +42,7 @@
 
     test('sendBytes after close received', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       channel.sendBytes(Uint8List(10));
       expect(await channel.events.toList(),
@@ -50,6 +53,7 @@
 
     test('sendText after close received', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       channel.sendText('Please close');
       expect(await channel.events.toList(),
@@ -60,6 +64,7 @@
 
     test('close after close received', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
 
       channel.sendText('Please close');
       expect(await channel.events.toList(),
@@ -71,6 +76,7 @@
     test('with invalid reason', () async {
       final channel =
           await channelFactory(uri.replace(queryParameters: {'badutf8': ''}));
+      addTearDown(() => closeWebSocket(channel));
 
       channel.sendText('Please close');
       final events = await channel.events.toList();
diff --git a/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_tests.dart
index b5f52e9..52970aa 100644
--- a/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_tests.dart
+++ b/pkgs/web_socket_conformance_tests/lib/src/disconnect_after_upgrade_tests.dart
@@ -9,6 +9,7 @@
 
 import 'disconnect_after_upgrade_server_vm.dart'
     if (dart.library.html) 'disconnect_after_upgrade_server_web.dart';
+import 'utils.dart';
 
 /// Tests that the [WebSocket] generates a correct [CloseReceived] event if
 /// the peer disconnects after WebSocket upgrade.
@@ -29,6 +30,7 @@
 
     test('disconnect after upgrade', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
       channel.sendText('test');
       expect(
           (await channel.events.single as CloseReceived).code,
diff --git a/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart
index 6b04b91..eaebeae 100644
--- a/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart
+++ b/pkgs/web_socket_conformance_tests/lib/src/payload_transfer_tests.dart
@@ -10,6 +10,7 @@
 import 'package:web_socket/web_socket.dart';
 
 import 'echo_server_vm.dart' if (dart.library.html) 'echo_server_web.dart';
+import 'utils.dart';
 
 /// Tests that the [WebSocket] can correctly transmit and receive text
 /// and binary payloads.
@@ -20,41 +21,48 @@
     late Uri uri;
     late StreamChannel<Object?> httpServerChannel;
     late StreamQueue<Object?> httpServerQueue;
-    late WebSocket webSocket;
 
     setUp(() async {
       httpServerChannel = await startServer();
       httpServerQueue = StreamQueue(httpServerChannel.stream);
       uri = Uri.parse('ws://localhost:${await httpServerQueue.next}');
-      webSocket = await webSocketFactory(uri);
     });
     tearDown(() async {
       httpServerChannel.sink.add(null);
-      await webSocket.close();
     });
 
     test('empty string request and response', () async {
+      final webSocket = await webSocketFactory(uri);
+      addTearDown(() => closeWebSocket(webSocket));
       webSocket.sendText('');
       expect(await webSocket.events.first, TextDataReceived(''));
     });
 
     test('empty binary request and response', () async {
+      final webSocket = await webSocketFactory(uri);
+      addTearDown(() => closeWebSocket(webSocket));
       webSocket.sendBytes(Uint8List(0));
       expect(await webSocket.events.first, BinaryDataReceived(Uint8List(0)));
     });
 
     test('string request and response', () async {
+      final webSocket = await webSocketFactory(uri);
+      addTearDown(() => closeWebSocket(webSocket));
       webSocket.sendText('Hello World!');
       expect(await webSocket.events.first, TextDataReceived('Hello World!'));
     });
 
     test('binary request and response', () async {
+      final webSocket = await webSocketFactory(uri);
+      addTearDown(() => closeWebSocket(webSocket));
       webSocket.sendBytes(Uint8List.fromList([1, 2, 3, 4, 5]));
       expect(await webSocket.events.first,
           BinaryDataReceived(Uint8List.fromList([1, 2, 3, 4, 5])));
     });
 
     test('large string request and response', () async {
+      final webSocket = await webSocketFactory(uri);
+      addTearDown(() => closeWebSocket(webSocket));
       final data = 'Hello World!' * 10000;
 
       webSocket.sendText(data);
@@ -62,6 +70,8 @@
     });
 
     test('large binary request and response', () async {
+      final webSocket = await webSocketFactory(uri);
+      addTearDown(() => closeWebSocket(webSocket));
       final data = Uint8List(1000000);
       data
         ..fillRange(0, data.length ~/ 10, 1)
@@ -80,11 +90,15 @@
     });
 
     test('non-ascii string request and response', () async {
+      final webSocket = await webSocketFactory(uri);
+      addTearDown(() => closeWebSocket(webSocket));
       webSocket.sendText('🎨⛳🌈');
       expect(await webSocket.events.first, TextDataReceived('🎨⛳🌈'));
     });
 
     test('alternative string and binary request and response', () async {
+      final webSocket = await webSocketFactory(uri);
+      addTearDown(() => closeWebSocket(webSocket));
       webSocket
         ..sendBytes(Uint8List.fromList([1]))
         ..sendText('Hello!')
diff --git a/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart
index ba44f51..52d2ec6 100644
--- a/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart
+++ b/pkgs/web_socket_conformance_tests/lib/src/peer_protocol_errors_tests.dart
@@ -9,6 +9,7 @@
 
 import 'peer_protocol_errors_server_vm.dart'
     if (dart.library.html) 'peer_protocol_errors_server_web.dart';
+import 'utils.dart';
 
 /// Tests that the [WebSocket] can correctly handle incorrect WebSocket frames.
 void testPeerProtocolErrors(
@@ -28,6 +29,7 @@
 
     test('bad data after upgrade', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
       expect(
           (await channel.events.single as CloseReceived).code,
           anyOf([
@@ -39,6 +41,7 @@
 
     test('bad data after upgrade with write', () async {
       final channel = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(channel));
       channel.sendText('test');
       expect(
           (await channel.events.single as CloseReceived).code,
diff --git a/pkgs/web_socket_conformance_tests/lib/src/protocol_tests.dart b/pkgs/web_socket_conformance_tests/lib/src/protocol_tests.dart
index 2d35f1c..3289507 100644
--- a/pkgs/web_socket_conformance_tests/lib/src/protocol_tests.dart
+++ b/pkgs/web_socket_conformance_tests/lib/src/protocol_tests.dart
@@ -9,6 +9,7 @@
 
 import 'protocol_server_vm.dart'
     if (dart.library.html) 'protocol_server_web.dart';
+import 'utils.dart';
 
 /// Tests that the [WebSocket] can correctly negotiate a subprotocol with the
 /// peer.
@@ -28,10 +29,13 @@
       httpServerQueue = StreamQueue(httpServerChannel.stream);
       uri = Uri.parse('ws://localhost:${await httpServerQueue.next}');
     });
-    tearDown(() => httpServerChannel.sink.add(null));
+    tearDown(() async {
+      httpServerChannel.sink.add(null);
+    });
 
     test('no protocol', () async {
       final socket = await channelFactory(uri);
+      addTearDown(() => closeWebSocket(socket));
 
       expect(await httpServerQueue.next, null);
       expect(socket.protocol, '');
@@ -42,6 +46,7 @@
       final socket = await channelFactory(
           uri.replace(queryParameters: {'protocol': 'chat.example.com'}),
           protocols: ['chat.example.com']);
+      addTearDown(() => closeWebSocket(socket));
 
       expect(await httpServerQueue.next, ['chat.example.com']);
       expect(socket.protocol, 'chat.example.com');
@@ -52,6 +57,7 @@
       final socket = await channelFactory(
           uri.replace(queryParameters: {'protocol': 'text.example.com'}),
           protocols: ['chat.example.com', 'text.example.com']);
+      addTearDown(() => closeWebSocket(socket));
 
       expect(
           await httpServerQueue.next, ['chat.example.com, text.example.com']);
diff --git a/pkgs/web_socket_conformance_tests/lib/src/utils.dart b/pkgs/web_socket_conformance_tests/lib/src/utils.dart
new file mode 100644
index 0000000..248b078
--- /dev/null
+++ b/pkgs/web_socket_conformance_tests/lib/src/utils.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2026, 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.
+
+import 'package:web_socket/web_socket.dart';
+
+/// Closes the [socket] and ignores [WebSocketConnectionClosed] if it's already
+/// closed.
+Future<void> closeWebSocket(WebSocket socket) async {
+  try {
+    await socket.close();
+  } on WebSocketConnectionClosed {
+    // Already closed.
+  }
+}