From 802f8c830522732383d9dbdcf9e3ea12e0a11972 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Thu, 1 Oct 2026 15:15:28 -0600 Subject: [PATCH] fix: stop late cable performs and empty coverage adds from flaking Playwright Cable: a component that unmounts while its connected handler is still refreshing the token performs on the closed consumer afterwards. anycable rejects that with NoConnectionError and never returns the promise, so it surfaced as an unhandled rejection (pageerror), flaking tlm-viewer "displays INST GRAPHS". Make perform/send no-ops once the subscription has been torn down. Coverage fixture: stopJSCoverage returns [] for pages without URL'd scripts or where profiling never started; MCR logs "The added coverage data must be Array(V8) or Object(Istanbul)" for that. Skip empty results. Co-Authored-By: Claude Opus 5.5 --- .../packages/openc3-js-common/src/services/cable.js | 13 +++++++++++++ playwright/tests/fixture.ts | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/openc3-cosmos-init/plugins/packages/openc3-js-common/src/services/cable.js b/openc3-cosmos-init/plugins/packages/openc3-js-common/src/services/cable.js index fc1ec780f7..4899bfb955 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-js-common/src/services/cable.js +++ b/openc3-cosmos-init/plugins/packages/openc3-js-common/src/services/cable.js @@ -63,11 +63,24 @@ class ResilientSubscription { return this._subscription?.identifier } + // Once the owner has torn this subscription down (unsubscribe, or + // Cable#disconnect when its component unmounts) there's nothing to perform + // on. Callers perform from async callbacks, e.g. after the token refresh in + // a connected handler, that can land after the teardown, and anycable + // rejects those on the closed consumer with NoConnectionError. Nobody awaits + // them, so that surfaced as an unhandled rejection - e.g. a Telemetry Viewer + // screen closed right after it opened. perform(action, data) { + if (this._unsubscribed) { + return Promise.resolve() + } return this._subscription?.perform(action, data) } send(data) { + if (this._unsubscribed) { + return Promise.resolve() + } return this._subscription?.send(data) } diff --git a/playwright/tests/fixture.ts b/playwright/tests/fixture.ts index ff207f7b33..2636eb321d 100644 --- a/playwright/tests/fixture.ts +++ b/playwright/tests/fixture.ts @@ -34,6 +34,10 @@ const startCoverage = async (page: any) => { const stopCoverage = async (page: any) => { try { const coverage = await page.coverage.stopJSCoverage() + // Empty when the page never loaded a script with a URL (e.g. a blank popup) + // or profiling never started on it. MCR rejects [] with "The added coverage + // data must be Array(V8) or Object(Istanbul)", so skip it. + if (!coverage.length) return // Appends raw V8 data to coverage/.cache (safe across workers AND // separate `playwright test` invocations); generate-coverage.mjs // merges everything into one report at the end of `pnpm test`