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(4); // 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 = 3'); // 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\(\)/); }); });