Firefox · DOM
CVE-2026-109770
Logic Error in DOM
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/webserial/SerialPort.cpp |
modified | |
ifdom/webserial/SerialPortParent.cpp |
modified | |
fordom/webserial/SerialPortParent.cpp |
modified | |
ifdom/webserial/tests/browser/browser_webserial_sharing_icon.js |
modified | |
add_setupdom/webserial/tests/browser/browser_webserial_sharing_icon.js |
modified | |
add_taskdom/webserial/tests/browser/browser_webserial_sharing_icon.js |
modified |
Files Changed
dom/webserial/PSerialPort.ipdldom/webserial/SerialPort.cppdom/webserial/SerialPort.hdom/webserial/SerialPortParent.cppdom/webserial/SerialPortParent.hdom/webserial/tests/browser/browser_webserial_permission_revoke.jsdom/webserial/tests/browser/browser_webserial_sharing_icon.js
Patch
diff --git a/dom/webserial/PSerialPort.ipdl b/dom/webserial/PSerialPort.ipdl
index 1494cc4a403..824dc81bb7d 100644
--- a/dom/webserial/PSerialPort.ipdl
+++ b/dom/webserial/PSerialPort.ipdl
@@ -48,9 +48,6 @@ parent:
// its WritableStream.
async AttachWritePipe(nullable DataPipeReceiver writePipeReceiver);
- // Notify parent of sharing state change
- async UpdateSharingState(bool connected);
-
// Clone the parent-side handle into a new PSerialPort actor, allowing
// a worker thread to bind its own child endpoint to the same port.
async Clone(Endpoint<PSerialPortParent> aEndpoint);
diff --git a/dom/webserial/SerialPort.cpp b/dom/webserial/SerialPort.cpp
index d5dcee6705a..442a09d4e96 100644
--- a/dom/webserial/SerialPort.cpp
+++ b/dom/webserial/SerialPort.cpp
@@ -285,7 +285,6 @@ already_AddRefed<Promise> SerialPort::Open(const SerialOptions& aOptions,
// Step 9.3: Set this.[[state]] to "opened".
self->mState = State::Opened;
self->UpdateWorkerRef();
- self->NotifySharingStateChanged(true);
// Step 9.4: Set this.[[bufferSize]].
self->mBufferSize = bufferSize;
self->mPipeCapacity = std::max(bufferSize, kMinSerialPortPumpSize);
@@ -559,7 +558,6 @@ already_AddRefed<Promise> SerialPort::Close(ErrorResult& aRv) {
aSelf->mState = State::Closed;
}
aSelf->UpdateWorkerRef();
- aSelf->NotifySharingStateChanged(false);
if (RefPtr<Promise> closePromise = aSelf->mClosePromise.forget()) {
closePromise->MaybeReject(aReason);
}
@@ -602,7 +600,6 @@ already_AddRefed<Promise> SerialPort::Forget(ErrorResult& aRv) {
}
UpdateWorkerRef();
- NotifySharingStateChanged(false);
if (mChild) {
RefPtr<SerialPortChild> child = mChild;
@@ -656,7 +653,6 @@ void SerialPort::MarkForgotten() {
}
UpdateWorkerRef();
- NotifySharingStateChanged(false);
}
void SerialPort::GetInfo(SerialPortInfo& aRetVal, ErrorResult& aRv) {
@@ -707,20 +703,6 @@ WritableStream* SerialPort::GetWritable() {
return mWritable;
}
-void SerialPort::NotifySharingStateChanged(bool aConnected) {
- if (!mChild) {
- return;
- }
-
- RefPtr<SerialPortChild> child = mChild;
- nsISerialEventTarget* actorTarget = child->GetActorEventTarget();
- if (actorTarget) {
- actorTarget->Dispatch(NS_NewRunnableFunction(
- "SerialPort::SendUpdateSharingState",
- [child, aConnected]() { child->SendUpdateSharingState(aConnected); }));
- }
-}
-
void SerialPort::OnActorDestroyed() {
if (mHasShutdown) {
return;
@@ -731,8 +713,7 @@ void SerialPort::OnActorDestroyed() {
NS_ConvertUTF16toUTF8(mInfo.id()).get()));
// Clear the child reference first since the actor is already destroyed.
- // This prevents MarkForgotten/NotifySharingStateChanged from trying to
- // use the dead actor.
+ // This prevents MarkForgotten from trying to use the dead actor.
mChild = nullptr;
// Mark the port as forgotten (closes streams, updates worker ref).
@@ -769,7 +750,6 @@ void SerialPort::NotifyDisconnected() {
// Don't have to wait for this
RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Graceful);
UpdateWorkerRef();
- NotifySharingStateChanged(false);
auto event = MakeRefPtr<Event>(this, nullptr, nullptr);
event->InitEvent(u"disconnect"_ns, true, false);
@@ -940,7 +920,6 @@ void SerialPort::SettleClosePromise(nsresult aResult) {
mState = State::Closed;
}
UpdateWorkerRef();
- NotifySharingStateChanged(false);
if (RefPtr<Promise> closePromise = mClosePromise.forget()) {
if (NS_SUCCEEDED(aResult)) {
closePromise->MaybeResolveWithUndefined();
diff --git a/dom/webserial/SerialPort.h b/dom/webserial/SerialPort.h
index 3950affdafe..67188e365e1 100644
--- a/dom/webserial/SerialPort.h
+++ b/dom/webserial/SerialPort.h
@@ -131,7 +131,6 @@ class SerialPort final : public DOMEventTargetHelper {
void SettleClosePromise(nsresult aResult);
MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CloseStreams(
StreamCloseMode aMode);
- void NotifySharingStateChanged(bool aConnected);
void UpdateWorkerRef();
RefPtr<Serial> mSerial;
diff --git a/dom/webserial/SerialPortParent.cpp b/dom/webserial/SerialPortParent.cpp
index 39792e025d3..4d180ea4a9b 100644
--- a/dom/webserial/SerialPortParent.cpp
+++ b/dom/webserial/SerialPortParent.cpp
@@ -104,6 +104,7 @@ mozilla::ipc::IPCResult SerialPortParent::RecvOpen(
NS_ConvertUTF16toUTF8(mPortId).get()));
mIsOpen = true;
+ NotifySharingStateChanged();
mPipeCapacity = std::max(aOptions.bufferSize(), kMinSerialPortPumpSize);
mDetectParityErrors = aOptions.parity() != ParityType::None;
@@ -144,6 +145,7 @@ mozilla::ipc::IPCResult SerialPortParent::RecvClose(CloseResolver&& aResolver) {
NS_ConvertUTF16toUTF8(mPortId).get(), static_cast<uint32_t>(rv)));
}
mIsOpen = false;
+ NotifySharingStateChanged();
aResolver(rv);
return IPC_OK();
@@ -409,12 +411,10 @@ mozilla::ipc::IPCResult SerialPortParent::RecvFlush(bool aReceive,
return IPC_OK();
}
-void SerialPortParent::NotifySharingStateChanged(bool aConnected) {
- mSharingConnected = aConnected;
-
+void SerialPortParent::NotifySharingStateChanged() {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"SerialPortParent::NotifySharingStateChanged",
- [browserId = mBrowserId, aConnected]() {
+ [browserId = mBrowserId, connected = mIsOpen]() {
nsCOMPtr<nsIObserverService> obs =
mozilla::services::GetObserverService();
if (!obs) {
@@ -423,26 +423,13 @@ void SerialPortParent::NotifySharingStateChanged(bool aConnected) {
auto props = MakeRefPtr<nsHashPropertyBag>();
props->SetPropertyAsUint64(u"browserId"_ns, browserId);
- props->SetPropertyAsBool(u"connected"_ns, aConnected);
+ props->SetPropertyAsBool(u"connected"_ns, connected);
obs->NotifyObservers(static_cast<nsIPropertyBag2*>(props),
"serial-device-state-changed", nullptr);
}));
}
-mozilla::ipc::IPCResult SerialPortParent::RecvUpdateSharingState(
- bool aConnected) {
- if (aConnected != mSharingConnected) {
- NotifySharingStateChanged(aConnected);
- } else {
- MOZ_LOG(gWebSerialLog, LogLevel::Warning,
- ("SerialPortParent[%p]::RecvUpdateSharingState got same state of "
- "%d for port '%s'",
- this, aConnected ? 1 : 0, NS_ConvertUTF16toUTF8(mPortId).get()));
- }
- return IPC_OK();
-}
-
mozilla::ipc::IPCResult SerialPortParent::RecvClone(
mozilla::ipc::Endpoint<PSerialPortParent>&& aEndpoint) {
if (!aEndpoint.IsValid()) {
@@ -478,6 +465,7 @@ void SerialPortParent::NotifyDisconnected() {
if (mIsOpen) {
StopPumpsBeforeClose();
mIsOpen = false;
+ NotifySharingStateChanged();
RefPtr<SerialPlatformService> service =
SerialPlatformService::GetInstance();
if (service) {
@@ -526,12 +514,10 @@ void SerialPortParent::ActorDestroy(ActorDestroyReason aWhy) {
mIsOpen = false;
}
- // If the child sent connected=true but never sent connected=false (e.g.
- // content process crash or iframe navigation), send the disconnect
- // notification so the browser sharing indicator count stays in sync.
- if (mSharingConnected) {
- NotifySharingStateChanged(false);
- }
+ // Covers the case where the port was still open when the actor went away
+ // (e.g. content process crash or iframe navigation) so the browser sharing
+ // indicator count stays in sync.
+ NotifySharingStateChanged();
nsTArray<RefPtr<SerialPortParent>> clones = std::move(mClones);
for (const auto& clone : clones) {
diff --git a/dom/webserial/SerialPortParent.h b/dom/webserial/SerialPortParent.h
index 6d826ef150e..c396d4c311c 100644
--- a/dom/webserial/SerialPortParent.h
+++ b/dom/webserial/SerialPortParent.h
@@ -40,7 +40,6 @@ class SerialPortParent final : public PSerialPortParent {
const RefPtr<mozilla::ipc::DataPipeSender>& aReadPipeSender);
mozilla::ipc::IPCResult RecvAttachWritePipe(
const RefPtr<mozilla::ipc::DataPipeReceiver>& aWritePipeReceiver);
- mozilla::ipc::IPCResult RecvUpdateSharingState(bool aConnected);
mozilla::ipc::IPCResult RecvClone(
mozilla::ipc::Endpoint<PSerialPortParent>&& aEndpoint);
@@ -66,7 +65,9 @@ class SerialPortParent final : public PSerialPortParent {
void StopReadPump();
void DestroyPlatformReader();
void StopWritePump();
- void NotifySharingStateChanged(bool aConnected);
+ // Should only be called when there's a transition from open->closed or
+ // closed->open, as otherwise the sharing indicator can get out of sync.
+ void NotifySharingStateChanged();
void StartReadPump(
already_AddRefed<mozilla::ipc::DataPipeSender> aReadPipeSender);
void StartWritePump(
@@ -75,10 +76,6 @@ class SerialPortParent final : public PSerialPortParent {
const nsString mPortId;
const uint64_t mBrowserId;
bool mIsOpen = false;
- // Whether we have forwarded a connected=true sharing state notification
- // without a corresponding connected=false. Used by ActorDestroy to send the
- // missing disconnect so the browser sharing indicator count stays in sync.
- bool mSharingConnected = false;
uint32_t mPipeCapacity = 0;
// Whether the port was opened with parity checking enabled (even/odd). When
// set, the read stream is wrapped to detect parity errors and surface them
diff --git a/dom/webserial/tests/browser/browser_webserial_permission_revoke.js b/dom/webserial/tests/browser/browser_webserial_permission_revoke.js
index d0accbefb62..28d9f7c8ac2 100644
--- a/dom/webserial/tests/browser/browser_webserial_permission_revoke.js
+++ b/dom/webserial/tests/browser/browser_webserial_permission_revoke.js
@@ -1,11 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-ChromeUtils.defineESModuleGetters(this, {
- SerialDeviceSharingHelper:
- "moz-src:///browser/modules/SerialDeviceSharingHelper.sys.mjs",
-});
-
const TEST_URL =
"https://example.com/document-builder.sjs?html=<h1>Test serial permission revocation</h1>";
@@ -54,8 +49,9 @@ add_task(async function testPermissionRevocation() {
gBrowser.selectedBrowser
);
- SerialDeviceSharingHelper.resetBrowserCount(gBrowser.selectedBrowser);
- gBrowser.updateBrowserSharing(gBrowser.selectedBrowser, { serial: false });
+ // Unlike the permission panel's revoke button, don't reset the sharing
+ // count here: the icon must be hidden purely by the parent process
+ // observing the port close that forget() triggers.
// Give time for observer to be registered (it happens async)
await new Promise(resolve => setTimeout(resolve, 0));
@@ -96,9 +92,11 @@ add_task(async function testPermissionRevocation() {
is(ports.length, 0, "getPorts() should return empty after revocation");
});
- let serialIcon = document.getElementById("serial-sharing-icon");
- ok(
- !BrowserTestUtils.isVisible(serialIcon),
+ await TestUtils.waitForCondition(
+ () =>
+ !BrowserTestUtils.isVisible(
+ document.getElementById("serial-sharing-icon")
+ ),
"Serial sharing icon should not be visible after permission revocation"
);
diff --git a/dom/webserial/tests/browser/browser_webserial_sharing_icon.js b/dom/webserial/tests/browser/browser_webserial_sharing_icon.js
index c9ababa37bf..2e219006020 100644
--- a/dom/webserial/tests/browser/browser_webserial_sharing_icon.js
+++ b/dom/webserial/tests/browser/browser_webserial_sharing_icon.js
@@ -3,6 +3,57 @@
const TEST_URL =
"https://example.com/document-builder.sjs?html=<h1>Test serial worker sharing icon</h1>";
+const BASE = "https://example.com/browser/dom/webserial/tests/browser/";
+const BLANK_URL = BASE + "blank.html";
+const WORKER_URL = BASE + "worker_open_close.js";
+
+function isIconVisible() {
+ return BrowserTestUtils.isVisible(
+ document.getElementById("serial-sharing-icon")
+ );
+}
+
+function waitForIconVisible(msg) {
+ return TestUtils.waitForCondition(isIconVisible, msg);
+}
+
+function waitForIconHidden(msg) {
+ return TestUtils.waitForCondition(() => !isIconVisible(), msg);
+}
+
+// Requests a port in the given browsing context (using autoselect, optionally
+// narrowed by filters) and opens it, keeping it reachable as content.testPort.
+async function requestAndOpenPort(bc, filters = []) {
+ await SpecialPowers.spawn(bc, [filters], async aFilters => {
+ content.navigator.serial.autoselectPorts = true;
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ content.testPort = await content.navigator.serial.requestPort({
+ filters: aFilters,
+ });
+ await content.testPort.open({ baudRate: 9600 });
+ });
+}
+
+// Starts a worker in the given browser that opens the first granted port and
+// resolves once it reports the port is open. The worker is kept reachable as
+// content.testWorker.
+async function openPortFromWorker(browser) {
+ await SpecialPowers.spawn(browser, [WORKER_URL], async url => {
+ const worker = new content.Worker(url);
+ content.testWorker = worker;
+ await new Promise((resolve, reject) => {
+ worker.onmessage = e => {
+ if (e.data.type === "opened") {
+ resolve();
+ } else if (e.data.type === "error") {
+ reject(new Error(e.data.message));
+ }
+ };
+ worker.onerror = e => reject(new Error(e.message));
+ worker.postMessage("open");
+ });
+ });
+}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
@@ -36,24 +87,7 @@ add_task(async function testSharingIconAppearsFromWorker() {
);
// Spawn a worker and have it open the port
- await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
- const worker = new content.Worker(
- "https://example.com/browser/dom/webserial/tests/browser/worker_open_close.js"
- );
- content.testWorker = worker;
-
- await new Promise((resolve, reject) => {
- worker.onmessage = e => {
- if (e.data.type === "opened") {
- resolve();
- } else if (e.data.type === "error") {
- reject(new Error(e.data.message));
- }
- };
- worker.onerror = e => reject(new Error(e.message));
- worker.postMessage("open");
- });
- });
+ await openPortFromWorker(gBrowser.selectedBrowser);
info("Waiting for serial sharing icon to become visible");
await TestUtils.waitForCondition(() => {
@@ -175,3 +209,217 @@ add_task(async function testSharingIconWithMultiplePorts() {
BrowserTestUtils.removeTab(tab);
});
+
+add_task(async function testSharingIconReopen() {
+ info("Test that the icon reappears when a port is closed and reopened");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ await requestAndOpenPort(gBrowser.selectedBrowser);
+ await waitForIconVisible("Icon should be visible after first open");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await content.testPort.close();
+ });
+ await waitForIconHidden("Icon should be hidden after first close");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await content.testPort.open({ baudRate: 9600 });
+ });
+ await waitForIconVisible("Icon should be visible again after reopen");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await content.testPort.close();
+ });
+ await waitForIconHidden("Icon should be hidden after second close");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSharingIconHiddenOnDeviceDisconnect() {
+ info("Test that the icon hides when an open port's device is unplugged");
+
+ const DEVICE_ID = "test-sharing-disconnect";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [DEVICE_ID], async id => {
+ await content.navigator.serial.simulateDeviceConnection(
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/dom/webserial/tests/browser/browser_webserial_permission_revoke.js b/dom/webserial/tests/browser/browser_webserial_permission_revoke.js
index d0accbefb62..28d9f7c8ac2 100644
--- a/dom/webserial/tests/browser/browser_webserial_permission_revoke.js
+++ b/dom/webserial/tests/browser/browser_webserial_permission_revoke.js
@@ -1,11 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-ChromeUtils.defineESModuleGetters(this, {
- SerialDeviceSharingHelper:
- "moz-src:///browser/modules/SerialDeviceSharingHelper.sys.mjs",
-});
-
const TEST_URL =
"https://example.com/document-builder.sjs?html=<h1>Test serial permission revocation</h1>";
@@ -54,8 +49,9 @@ add_task(async function testPermissionRevocation() {
gBrowser.selectedBrowser
);
- SerialDeviceSharingHelper.resetBrowserCount(gBrowser.selectedBrowser);
- gBrowser.updateBrowserSharing(gBrowser.selectedBrowser, { serial: false });
+ // Unlike the permission panel's revoke button, don't reset the sharing
+ // count here: the icon must be hidden purely by the parent process
+ // observing the port close that forget() triggers.
// Give time for observer to be registered (it happens async)
await new Promise(resolve => setTimeout(resolve, 0));
@@ -96,9 +92,11 @@ add_task(async function testPermissionRevocation() {
is(ports.length, 0, "getPorts() should return empty after revocation");
});
- let serialIcon = document.getElementById("serial-sharing-icon");
- ok(
- !BrowserTestUtils.isVisible(serialIcon),
+ await TestUtils.waitForCondition(
+ () =>
+ !BrowserTestUtils.isVisible(
+ document.getElementById("serial-sharing-icon")
+ ),
"Serial sharing icon should not be visible after permission revocation"
);
diff --git a/dom/webserial/tests/browser/browser_webserial_sharing_icon.js b/dom/webserial/tests/browser/browser_webserial_sharing_icon.js
index c9ababa37bf..2e219006020 100644
--- a/dom/webserial/tests/browser/browser_webserial_sharing_icon.js
+++ b/dom/webserial/tests/browser/browser_webserial_sharing_icon.js
@@ -3,6 +3,57 @@
const TEST_URL =
"https://example.com/document-builder.sjs?html=<h1>Test serial worker sharing icon</h1>";
+const BASE = "https://example.com/browser/dom/webserial/tests/browser/";
+const BLANK_URL = BASE + "blank.html";
+const WORKER_URL = BASE + "worker_open_close.js";
+
+function isIconVisible() {
+ return BrowserTestUtils.isVisible(
+ document.getElementById("serial-sharing-icon")
+ );
+}
+
+function waitForIconVisible(msg) {
+ return TestUtils.waitForCondition(isIconVisible, msg);
+}
+
+function waitForIconHidden(msg) {
+ return TestUtils.waitForCondition(() => !isIconVisible(), msg);
+}
+
+// Requests a port in the given browsing context (using autoselect, optionally
+// narrowed by filters) and opens it, keeping it reachable as content.testPort.
+async function requestAndOpenPort(bc, filters = []) {
+ await SpecialPowers.spawn(bc, [filters], async aFilters => {
+ content.navigator.serial.autoselectPorts = true;
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ content.testPort = await content.navigator.serial.requestPort({
+ filters: aFilters,
+ });
+ await content.testPort.open({ baudRate: 9600 });
+ });
+}
+
+// Starts a worker in the given browser that opens the first granted port and
+// resolves once it reports the port is open. The worker is kept reachable as
+// content.testWorker.
+async function openPortFromWorker(browser) {
+ await SpecialPowers.spawn(browser, [WORKER_URL], async url => {
+ const worker = new content.Worker(url);
+ content.testWorker = worker;
+ await new Promise((resolve, reject) => {
+ worker.onmessage = e => {
+ if (e.data.type === "opened") {
+ resolve();
+ } else if (e.data.type === "error") {
+ reject(new Error(e.data.message));
+ }
+ };
+ worker.onerror = e => reject(new Error(e.message));
+ worker.postMessage("open");
+ });
+ });
+}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
@@ -36,24 +87,7 @@ add_task(async function testSharingIconAppearsFromWorker() {
);
// Spawn a worker and have it open the port
- await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
- const worker = new content.Worker(
- "https://example.com/browser/dom/webserial/tests/browser/worker_open_close.js"
- );
- content.testWorker = worker;
-
- await new Promise((resolve, reject) => {
- worker.onmessage = e => {
- if (e.data.type === "opened") {
- resolve();
- } else if (e.data.type === "error") {
- reject(new Error(e.data.message));
- }
- };
- worker.onerror = e => reject(new Error(e.message));
- worker.postMessage("open");
- });
- });
+ await openPortFromWorker(gBrowser.selectedBrowser);
info("Waiting for serial sharing icon to become visible");
await TestUtils.waitForCondition(() => {
@@ -175,3 +209,217 @@ add_task(async function testSharingIconWithMultiplePorts() {
BrowserTestUtils.removeTab(tab);
});
+
+add_task(async function testSharingIconReopen() {
+ info("Test that the icon reappears when a port is closed and reopened");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ await requestAndOpenPort(gBrowser.selectedBrowser);
+ await waitForIconVisible("Icon should be visible after first open");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await content.testPort.close();
+ });
+ await waitForIconHidden("Icon should be hidden after first close");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await content.testPort.open({ baudRate: 9600 });
+ });
+ await waitForIconVisible("Icon should be visible again after reopen");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await content.testPort.close();
+ });
+ await waitForIconHidden("Icon should be hidden after second close");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSharingIconHiddenOnDeviceDisconnect() {
+ info("Test that the icon hides when an open port's device is unplugged");
+
+ const DEVICE_ID = "test-sharing-disconnect";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [DEVICE_ID], async id => {
+ await content.navigator.serial.simulateDeviceConnection(
+ id,
+ "/dev/ttyUSB90",
+ 0x1111,
+ 0x2222
+ );
+ content.navigator.serial.autoselectPorts = true;
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ content.testPort = await content.navigator.serial.requestPort({
+ filters: [{ usbVendorId: 0x1111, usbProductId: 0x2222 }],
+ });
+ await content.testPort.open({ baudRate: 9600 });
+ });
+ await waitForIconVisible("Icon should be visible while the port is open");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [DEVICE_ID], async id => {
+ const disconnected = new Promise(resolve => {
+ content.testPort.addEventListener("disconnect", resolve, { once: true });
+ });
+ await content.navigator.serial.simulateDeviceDisconnection(id);
+ await disconnected;
+ });
+ await waitForIconHidden("Icon should be hidden after the device disconnects");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSharingIconHiddenOnNavigation() {
+ info("Test that the icon hides when a page with an open port navigates away");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ await requestAndOpenPort(gBrowser.selectedBrowser);
+ await waitForIconVisible("Icon should be visible while the port is open");
+
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, BLANK_URL);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await waitForIconHidden("Icon should be hidden after navigating away");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSharingIconHiddenOnWorkerTerminate() {
+ info("Test that the icon hides when a worker holding an open port is killed");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ content.navigator.serial.autoselectPorts = true;
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ await content.navigator.serial.requestPort();
+ });
+
+ await openPortFromWorker(gBrowser.selectedBrowser);
+ await waitForIconVisible(
+ "Icon should be visible while the worker's port is open"
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.testWorker.terminate();
+ });
+ await waitForIconHidden(
+ "Icon should be hidden after terminating the worker without closing"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSharingIconHiddenOnIframeRemoval() {
+ info(
+ "Test that a port opened in an iframe shows the icon and hides on removal"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ let browser = gBrowser.selectedBrowser;
+
+ await SpecialPowers.spawn(browser, [BLANK_URL], async url => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = url;
+ const loaded = new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ });
+ content.document.body.appendChild(iframe);
+ await loaded;
+ });
+
+ // Granted ports are tracked per window, so the same-origin iframe requests
+ // and opens its own port. The sharing state must be attributed to the
+ // top-level browser.
+ let iframeBC = browser.browsingContext.children[0];
+ ok(iframeBC, "Iframe browsing context should exist");
+ await requestAndOpenPort(iframeBC);
+ await waitForIconVisible(
+ "Icon should be visible while the iframe's port is open"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("iframe").remove();
+ });
+ await waitForIconHidden("Icon should be hidden after the iframe is removed");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testSharingStateIsPerTab() {
+ info("Test that closing a port in one tab does not affect another tab");
+
+ // Each tab must open a different device; a port can only be open once.
+ let tabA = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ await requestAndOpenPort(tabA.linkedBrowser, [
+ { usbVendorId: 0x2341, usbProductId: 0x0043 },
+ ]);
+ await waitForIconVisible("Icon should be visible for tab A");
+
+ let tabB = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ await requestAndOpenPort(tabB.linkedBrowser, [
+ { usbVendorId: 0x0403, usbProductId: 0x6002 },
+ ]);
+ await waitForIconVisible("Icon should be visible for tab B");
+
+ await SpecialPowers.spawn(tabA.linkedBrowser, [], async () => {
+ await content.testPort.close();
+ });
+ await TestUtils.waitForCondition(
+ () => !tabA.linkedBrowser._sharingState?.serial,
+ "Tab A sharing state should be cleared"
+ );
+ is(
+ tabB.linkedBrowser._sharingState?.serial,
+ "serial",
+ "Tab B sharing state should be unaffected"
+ );
+ ok(isIconVisible(), "Icon should remain visible for the selected tab B");
+
+ await BrowserTestUtils.switchTab(gBrowser, tabA);
... (truncated)
Loading diff…
References
On This Page