mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-18 15:23:13 +00:00
fix(extension): harden target and chat recovery
This commit is contained in:
@@ -4,6 +4,28 @@ All notable changes to the KoalaSync browser extension and relay server.
|
||||
|
||||
---
|
||||
|
||||
## [v3.1.3] — Unreleased
|
||||
|
||||
This release hardens target selection and chat visibility across player-frame
|
||||
recovery, failed tab switches and short connection interruptions.
|
||||
|
||||
### Fixed
|
||||
- **Extension: Atomic target switching** — Keeps the previously working target
|
||||
active until the newly selected tab has been injected successfully, so a
|
||||
failed selection cannot leave the room without a target.
|
||||
- **Extension: Target recovery** — Keeps the selected tab recoverable when a
|
||||
transient content-script or embedded-player refresh fails.
|
||||
- **Extension: Chat visibility persistence** — Remembers the user's manual
|
||||
open/closed state across popup reopen, content refresh and reconnect cycles.
|
||||
|
||||
### Testing
|
||||
- **Release gate** — Unit, server/WebSocket, locale, theme, lint, production
|
||||
dependency audit, Chrome/Firefox build, AMO validation and website build
|
||||
pass locally.
|
||||
- **Browser E2E** — 35 extension and player lifecycle scenarios pass.
|
||||
|
||||
---
|
||||
|
||||
## [v3.1.2] — 2026-08-17
|
||||
|
||||
This release adds generic control for HTML5 players inside cross-origin frames.
|
||||
|
||||
+20
-21
@@ -2658,9 +2658,6 @@ async function activateTargetTab(tabId, tabTitle, {
|
||||
let injectedContentTarget = { frameId: 0, documentId: null, hasVideo: false };
|
||||
|
||||
try {
|
||||
if (previousTabId && previousTabId !== selectedTabId) {
|
||||
await deactivateTargetTab(previousTabId);
|
||||
}
|
||||
if (activationGeneration !== targetActivationGeneration) {
|
||||
return { status: 'superseded' };
|
||||
}
|
||||
@@ -2696,26 +2693,26 @@ async function activateTargetTab(tabId, tabTitle, {
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
currentTabId = null;
|
||||
currentTabTitle = null;
|
||||
clearCurrentContentTarget();
|
||||
lastContentHeartbeatAt = null;
|
||||
if (currentRoom) roomIdleSince = Date.now();
|
||||
const failedContentTarget = error?.contentTarget || injectedContentTarget;
|
||||
await deactivateTargetTab(selectedTabId, failedContentTarget);
|
||||
if (previousTabId && (previousTabId !== selectedTabId
|
||||
|| !sameContentTarget(previousContentTarget, failedContentTarget))) {
|
||||
await deactivateTargetTab(previousTabId, previousContentTarget);
|
||||
if (previousTabId === null) {
|
||||
currentTabId = null;
|
||||
currentTabTitle = null;
|
||||
clearCurrentContentTarget();
|
||||
lastContentHeartbeatAt = null;
|
||||
if (currentRoom) roomIdleSince = Date.now();
|
||||
await chrome.storage.session.set({
|
||||
currentTabId: null,
|
||||
currentTabTitle: null,
|
||||
currentTargetFrameId: 0,
|
||||
currentTargetDocumentId: null,
|
||||
currentTargetHasVideo: false,
|
||||
roomIdleSince,
|
||||
lastContentHeartbeatAt: null
|
||||
});
|
||||
} else {
|
||||
addLog(`Target switch to tab ${selectedTabId} failed; keeping tab ${previousTabId} selected`, 'warn');
|
||||
}
|
||||
await chrome.storage.session.set({
|
||||
currentTabId: null,
|
||||
currentTabTitle: null,
|
||||
currentTargetFrameId: 0,
|
||||
currentTargetDocumentId: null,
|
||||
currentTargetHasVideo: false,
|
||||
roomIdleSince,
|
||||
lastContentHeartbeatAt: null
|
||||
});
|
||||
if (activationGeneration !== targetActivationGeneration) {
|
||||
return { status: 'superseded' };
|
||||
}
|
||||
@@ -2765,7 +2762,9 @@ async function activateTargetTab(tabId, tabTitle, {
|
||||
if (currentTabId !== selectedTabId) await deactivateTargetTab(selectedTabId, injectedContentTarget);
|
||||
return { status: 'superseded' };
|
||||
}
|
||||
if (previousTabId === selectedTabId
|
||||
if (previousTabId && previousTabId !== selectedTabId) {
|
||||
await deactivateTargetTab(previousTabId, previousContentTarget);
|
||||
} else if (previousTabId === selectedTabId
|
||||
&& !sameContentTarget(previousContentTarget, injectedContentTarget)) {
|
||||
await deactivateTargetTab(previousTabId, previousContentTarget, { deactivateMonitor: false });
|
||||
}
|
||||
|
||||
@@ -181,6 +181,8 @@ describe('chat overlay contract', () => {
|
||||
expect(overlaySource).toContain('setOpened(false, false)');
|
||||
expect(overlaySource).toContain('setOpened(lastUserOpenState ?? (chatStartMode === \'open\'), false)');
|
||||
expect(overlaySource).toContain('typeof data[openStateKey] === \'boolean\'');
|
||||
expect(overlaySource).toContain('const previousEnabled = context?.enabled === true');
|
||||
expect(overlaySource).toContain('(!startStateApplied || !previousEnabled)');
|
||||
});
|
||||
|
||||
it('keeps chat hidden by default without discarding the room chat key', () => {
|
||||
|
||||
@@ -596,6 +596,7 @@
|
||||
|
||||
function applyContext(next) {
|
||||
const previousRoomId = context?.roomId;
|
||||
const previousEnabled = context?.enabled === true;
|
||||
context = next || null;
|
||||
const supported = !!context?.supported;
|
||||
const optedIn = !!context?.enabled;
|
||||
@@ -611,7 +612,7 @@
|
||||
if (!optedIn) startStateApplied = false;
|
||||
if (!context?.enabled) {
|
||||
setOpened(false, false);
|
||||
} else if (preferencesLoaded && !startStateApplied) {
|
||||
} else if (preferencesLoaded && (!startStateApplied || !previousEnabled)) {
|
||||
startStateApplied = true;
|
||||
setOpened(lastUserOpenState ?? (chatStartMode === 'open'), false);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ describe('target tab lifecycle', () => {
|
||||
const activationStart = backgroundSource.indexOf('async function activateTargetTab');
|
||||
const activationEnd = backgroundSource.indexOf('async function reactivateCurrentTarget', activationStart);
|
||||
const activationSource = backgroundSource.slice(activationStart, activationEnd);
|
||||
expect(activationSource.indexOf('await deactivateTargetTab(previousTabId)'))
|
||||
.toBeLessThan(activationSource.indexOf('await injectContentScript(selectedTabId'));
|
||||
expect(activationSource.indexOf('await injectContentScript(selectedTabId'))
|
||||
.toBeLessThan(activationSource.indexOf('await deactivateTargetTab(previousTabId, previousContentTarget)'));
|
||||
expect(activationSource).toContain('previousTabId !== selectedTabId');
|
||||
expect(activationSource).toContain('keeping tab ${previousTabId} selected');
|
||||
expect(contentSource).toContain('if (window.koalaSyncInjected && chrome.runtime.id)');
|
||||
expect(overlaySource).toContain('if (window.koalaSyncChatOverlay?.refresh)');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user