mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-19 15:46:15 +00:00
f450584562
getReadyTabVideoState() treated "no video found" as a broken injection and forced a full reactivation. On a page that legitimately has no video yet — an anime or Drive page before playback starts — that fired on every call, and the dev panel polls it on a timer. The result was an endless teardown and reinjection cycle: the target never settled, the popup showed "activating" forever, and the panel reported "Target tab changed before content script recovery completed" because each read raced the reactivation it had triggered. Only an unreachable content script justifies recovery now, and that recovery no longer reinjects unless the selected frame actually moved. Audited against v3.1.2, which worked on these pages. The only unjustified deviation left was the retry budget, which had been cut from eight passes to three and shortened the window for a late-loading player; it is back at eight, now bounded by a wall-clock deadline instead of being unbounded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
10 KiB
JavaScript
158 lines
10 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const backgroundSource = fs.readFileSync(path.join(extensionDir, 'background.js'), 'utf8');
|
|
const contentSource = fs.readFileSync(path.join(extensionDir, 'content.js'), 'utf8');
|
|
const overlaySource = fs.readFileSync(path.join(extensionDir, 'chat-overlay.js'), 'utf8');
|
|
const monitorSource = fs.readFileSync(path.join(extensionDir, 'media-frame-monitor.js'), 'utf8');
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(extensionDir, 'manifest.base.json'), 'utf8'));
|
|
|
|
describe('target tab lifecycle', () => {
|
|
it('injects playback and chat scripts only into the explicitly selected tab', () => {
|
|
expect(backgroundSource).not.toContain('chrome.tabs.onActivated');
|
|
expect(backgroundSource).not.toContain('chrome.tabs.query({})');
|
|
expect(backgroundSource).toContain('contentTarget = await resolveMediaContentTarget(chrome, tabId)');
|
|
expect(backgroundSource).toContain('target: scriptTarget');
|
|
expect(backgroundSource).toContain("files: ['chat-format.js', 'chat-overlay.js', 'content.js']");
|
|
expect(backgroundSource).toContain("chrome.tabs.query({ url: 'https://sync.koalastuff.net/*' })");
|
|
|
|
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).toContain('previousTabId !== selectedTabId');
|
|
expect(contentSource).toContain('if (window.koalaSyncInjected && chrome.runtime.id)');
|
|
expect(overlaySource).toContain('if (window.koalaSyncChatOverlay?.refresh)');
|
|
});
|
|
|
|
it('keeps the chat overlay in the top document when the player is nested', () => {
|
|
expect(backgroundSource).toContain('function sendMessageToChatOverlay(message)');
|
|
expect(backgroundSource).toContain('return sendMessageToFrame(tabId, 0, message)');
|
|
// Every chat-facing message must reach the overlay's frame, not the
|
|
// player's. A stray sendMessageToCurrentContent here renders the chat
|
|
// inside the video on Drive.
|
|
expect(backgroundSource).not.toMatch(/sendMessageToCurrentContent\(\{\s*type: 'CHAT/);
|
|
expect(backgroundSource).toMatch(
|
|
/target: \{ tabId, frameIds: \[0\] \},\s*files: \['chat-format\.js', 'chat-overlay\.js'\]/
|
|
);
|
|
expect(backgroundSource).toContain("files: ['content.js']");
|
|
expect(backgroundSource).toContain('if (normalizeFrameId(target.frameId) !== 0)');
|
|
});
|
|
|
|
it('does not reactivate the target for ordinary playback churn', () => {
|
|
expect(backgroundSource).toContain('async function selectedMediaTargetMoved(tabId)');
|
|
expect(backgroundSource).toContain('onlyIfTargetMoved = false');
|
|
expect(backgroundSource.match(/onlyIfTargetMoved: true/g)?.length).toBe(5);
|
|
// Playback state must stay out of the candidate signature, otherwise
|
|
// every play/pause looks like a frame layout change.
|
|
expect(monitorSource).not.toContain('element.paused ? 0 : 1');
|
|
expect(monitorSource).toContain('element.readyState > 0 ? 1 : 0');
|
|
});
|
|
|
|
it('bounds every frame probe and verifies withheld origins', () => {
|
|
const resolverSource = fs.readFileSync(
|
|
path.join(extensionDir, 'media-frame-target.js'),
|
|
'utf8'
|
|
);
|
|
expect(resolverSource).toContain('async function originAccessIsWithheld(chromeApi, originPattern)');
|
|
expect(resolverSource).toContain('probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS');
|
|
expect(resolverSource).toContain('attempts = 8');
|
|
expect(resolverSource).toContain('deadlineMs = 12000');
|
|
// A swallowed probe error is what turned a slow player frame into a
|
|
// permission prompt for an origin the extension already held.
|
|
expect(resolverSource).toContain('errors.push({ target, error })');
|
|
});
|
|
|
|
it('fully deactivates old and superseded target injections', () => {
|
|
expect(backgroundSource).toContain("{ type: 'TARGET_DEACTIVATE' }");
|
|
expect(backgroundSource).toContain('target.documentId');
|
|
expect(backgroundSource.match(/await deactivateTargetTab\(selectedTabId,/g)?.length).toBeGreaterThanOrEqual(6);
|
|
expect(contentSource).toContain("if (message.type === 'TARGET_DEACTIVATE')");
|
|
expect(overlaySource).toContain("message?.type === 'TARGET_DEACTIVATE'");
|
|
});
|
|
|
|
it('removes monitors injected by a superseded cross-tab activation', () => {
|
|
expect(backgroundSource).toContain('function isTargetActivationSuperseded(tabId, activationGeneration)');
|
|
expect(backgroundSource).toMatch(/navigationRetries: navigationRetries - 1,\s*activationGeneration\s*\}\)/);
|
|
expect(backgroundSource).toMatch(/await injectMediaFrameMonitors\(tabId, contentTarget\);[\s\S]*if \(isTargetActivationSuperseded\(tabId, activationGeneration\)\)[\s\S]*await deactivateMediaFrameMonitors\(tabId\);/);
|
|
expect(backgroundSource).toContain("error.code = 'target_activation_superseded'");
|
|
});
|
|
|
|
it('uses all-frame probing for cross-origin targets without navigation permissions', () => {
|
|
expect(backgroundSource).toContain("files: ['media-frame-monitor.js']");
|
|
expect(backgroundSource).toContain('const targets = listMediaFrameScriptTargets(tabId)');
|
|
expect(backgroundSource).toContain('One denied widget frame must not block the selected player');
|
|
expect(backgroundSource).toContain("navigationError.code = 'media_target_navigated'");
|
|
expect(backgroundSource).toContain("{ type: 'MEDIA_MONITOR_DEACTIVATE' }");
|
|
expect(backgroundSource).toContain('async function deactivateMediaFrameMonitors(tabId)');
|
|
expect(backgroundSource).toContain('{ documentId }');
|
|
expect(monitorSource).toContain("type: 'MEDIA_FRAME_CANDIDATE_CHANGED'");
|
|
expect(monitorSource).toContain("attributeFilter: ['class', 'style', 'hidden', 'src', 'controls']");
|
|
expect(monitorSource).toContain('if (!force && nextSignature === lastCandidateSignature) return');
|
|
expect(monitorSource).toContain("const MEDIA_STATE_EVENTS = ['play', 'pause', 'loadedmetadata'");
|
|
expect(monitorSource).toContain("node.querySelector?.('video, iframe, frame')");
|
|
expect(manifest.permissions).toEqual([
|
|
'storage',
|
|
'tabs',
|
|
'scripting',
|
|
'alarms',
|
|
'activeTab',
|
|
'notifications'
|
|
]);
|
|
expect(backgroundSource).not.toMatch(/chrome\.(?:web)?Navigation/);
|
|
});
|
|
|
|
it('serializes content commands and coalesces target refreshes', () => {
|
|
expect(backgroundSource).toContain('contentCommandQueue.catch(() => {}).then(deliver)');
|
|
expect(backgroundSource).toContain('if (mediaTargetRefreshTask && mediaTargetRefreshTabId === selectedTabId)');
|
|
expect(backgroundSource).toContain('if (queueIfRunning) mediaTargetRefreshDirty = true');
|
|
expect(backgroundSource).toContain('&& pass < 2');
|
|
expect(backgroundSource).toContain('const needsFollowup = mediaTargetRefreshDirty');
|
|
expect(backgroundSource).not.toContain('Re-elect before every remote command');
|
|
expect(backgroundSource).toContain('await sendMessageToContentTab(tabId');
|
|
});
|
|
|
|
it('tears down every persistent content-script resource', () => {
|
|
expect(contentSource).toContain('function destroyContentScript({ preserveAudioRoute = false } = {})');
|
|
// Deselecting a tab hands the page back to itself; it must not go mute.
|
|
expect(contentSource).toContain('destroyContentScript({ preserveAudioRoute: true });');
|
|
expect(contentSource).toContain('if (!preserveAudioRoute) closeAudioContext();');
|
|
expect(contentSource).toContain('observer.disconnect()');
|
|
expect(contentSource).toContain('keepAlivePort.disconnect()');
|
|
expect(contentSource).toContain('for (const video of [...attachedVideos]) detachVideoListeners(video);');
|
|
expect(contentSource).toContain("document.removeEventListener('visibilitychange', handleVisibilityChange)");
|
|
expect(contentSource).toContain("window.removeEventListener('pagehide', handlePageHide)");
|
|
expect(contentSource).toContain("window.removeEventListener('pageshow', handlePageShow)");
|
|
expect(contentSource).toContain("window.removeEventListener('resize', handleMediaFrameResize)");
|
|
expect(contentSource).toContain('chrome.storage.onChanged.removeListener(handleStorageChanged)');
|
|
expect(contentSource).toContain('chrome.runtime.onMessage.removeListener(handleRuntimeMessage)');
|
|
expect(contentSource).toContain('window.koalaSyncInjected = false');
|
|
});
|
|
|
|
it('stops the MAIN-world seek bridge when its target is deactivated', () => {
|
|
expect(backgroundSource).toContain('const timelineInterval = setInterval');
|
|
expect(backgroundSource).toContain('clearInterval(timelineInterval)');
|
|
expect(backgroundSource).toContain("window.removeEventListener('message', handleBridgeMessage)");
|
|
expect(contentSource).toContain("kind: 'destroy'");
|
|
});
|
|
|
|
it('self-cleans instead of throwing when an extension reload invalidates the context', () => {
|
|
expect(contentSource).toContain('if (!chrome.runtime?.id)');
|
|
expect(contentSource).toContain('destroyContentScript()');
|
|
expect(contentSource).toContain('return chrome.runtime.sendMessage(message, callback) || Promise.resolve(undefined)');
|
|
expect(contentSource).toContain('function handleVisibilityChange()');
|
|
});
|
|
|
|
it('suspends background work for bfcache and restores it without duplicate injection', () => {
|
|
expect(contentSource).toContain('pageSuspended = true');
|
|
expect(contentSource).toContain('if (destroyed || pageSuspended) return');
|
|
expect(contentSource).toContain('if (!destroyed && !pageSuspended) scheduleLifecycleTimeout(connectKeepAlivePort, 1000)');
|
|
expect(contentSource).toMatch(/function handlePageShow\(event\)[\s\S]*if \(!event\.persisted\) return;[\s\S]*pageSuspended = false/);
|
|
expect(contentSource).toMatch(/function handlePageShow\(event\)[\s\S]*observer\.observe\(document\.documentElement[\s\S]*setupListeners\(\)[\s\S]*connectKeepAlivePort\(\)/);
|
|
});
|
|
});
|