mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-09-02 05:57:55 +00:00
fix(extension): harden Netflix page API sync
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it, vi } 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 providerSource = fs.readFileSync(path.join(extensionDir, 'page-api-seek-overrides.js'), 'utf8');
|
||||
|
||||
function extractFunction(source, name) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const bodyStart = source.indexOf('{', start);
|
||||
let depth = 0;
|
||||
for (let index = bodyStart; index < source.length; index++) {
|
||||
if (source[index] === '{') depth++;
|
||||
if (source[index] === '}') depth--;
|
||||
if (depth === 0) return source.slice(start, index + 1);
|
||||
}
|
||||
throw new Error(`${name} body did not terminate`);
|
||||
}
|
||||
|
||||
function createBridgeHarness({ sessionIds = ['watch-main'], players = {} } = {}) {
|
||||
const listeners = new Set();
|
||||
const posted = [];
|
||||
const fakeWindow = {
|
||||
location: { hostname: 'www.netflix.com' },
|
||||
koalaFindPageApiSeekProvider: () => ({
|
||||
provider: 'netflix',
|
||||
actions: ['play', 'pause', 'seek']
|
||||
}),
|
||||
netflix: {
|
||||
appContext: {
|
||||
state: {
|
||||
playerApp: {
|
||||
getAPI: () => ({
|
||||
videoPlayer: {
|
||||
getAllPlayerSessionIds: () => sessionIds,
|
||||
getVideoPlayerBySessionId: id => players[id] || null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addEventListener(type, listener) {
|
||||
if (type === 'message') listeners.add(listener);
|
||||
},
|
||||
removeEventListener(type, listener) {
|
||||
if (type === 'message') listeners.delete(listener);
|
||||
},
|
||||
postMessage(data) {
|
||||
posted.push(data);
|
||||
}
|
||||
};
|
||||
const installSource = extractFunction(backgroundSource, 'installPageApiPlayerBridge');
|
||||
const install = Function(
|
||||
'window',
|
||||
'document',
|
||||
'setInterval',
|
||||
'clearInterval',
|
||||
`'use strict'; ${installSource}; return installPageApiPlayerBridge;`
|
||||
)(
|
||||
fakeWindow,
|
||||
{ querySelector: () => null },
|
||||
() => 1,
|
||||
() => {}
|
||||
);
|
||||
install();
|
||||
return {
|
||||
fakeWindow,
|
||||
posted,
|
||||
dispatch(data) {
|
||||
for (const listener of [...listeners]) listener({ source: fakeWindow, data });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function sendCommand(harness, action, time = null) {
|
||||
const requestId = `request-${action}`;
|
||||
harness.dispatch({
|
||||
__koalaPageApiPlayer: 1,
|
||||
kind: 'command',
|
||||
requestId,
|
||||
action,
|
||||
time
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(harness.posted.some(message => message.kind === 'result' && message.requestId === requestId)).toBe(true);
|
||||
});
|
||||
return harness.posted.find(message => message.kind === 'result' && message.requestId === requestId);
|
||||
}
|
||||
|
||||
describe('page API player bridge', () => {
|
||||
it('declares every Netflix write as bridge-only while Disney remains seek-only', () => {
|
||||
const root = {};
|
||||
vm.runInNewContext(providerSource, { globalThis: root, URL });
|
||||
expect(root.koalaFindPageApiSeekProvider('https://www.netflix.com/watch/1')).toMatchObject({
|
||||
provider: 'netflix',
|
||||
actions: ['play', 'pause', 'seek']
|
||||
});
|
||||
expect(root.koalaFindPageApiSeekProvider('www.disneyplus.com')).toMatchObject({
|
||||
provider: 'disney',
|
||||
actions: ['seek']
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers the Netflix watch session and confirms play, pause, and seek', async () => {
|
||||
const preview = { seek: vi.fn(), play: vi.fn(), pause: vi.fn() };
|
||||
const player = { seek: vi.fn(), play: vi.fn(), pause: vi.fn() };
|
||||
const harness = createBridgeHarness({
|
||||
sessionIds: ['motion-billboard-1', 'watch-main'],
|
||||
players: { 'motion-billboard-1': preview, 'watch-main': player }
|
||||
});
|
||||
|
||||
await expect(sendCommand(harness, 'seek', 12.345)).resolves.toMatchObject({ ok: true });
|
||||
await expect(sendCommand(harness, 'pause')).resolves.toMatchObject({ ok: true });
|
||||
await expect(sendCommand(harness, 'play')).resolves.toMatchObject({ ok: true });
|
||||
expect(player.seek).toHaveBeenCalledWith(12345);
|
||||
expect(player.pause).toHaveBeenCalledOnce();
|
||||
expect(player.play).toHaveBeenCalledOnce();
|
||||
expect(preview.seek).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns an explicit failure when the Netflix player is unavailable', async () => {
|
||||
const harness = createBridgeHarness({ sessionIds: ['watch-missing'], players: {} });
|
||||
await expect(sendCommand(harness, 'seek', 10)).resolves.toMatchObject({
|
||||
ok: false,
|
||||
reason: 'player_unavailable'
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps provider actions fail-closed and ACKs only applied remote commands', () => {
|
||||
const seekVideo = extractFunction(contentSource, 'seekVideo');
|
||||
const request = extractFunction(contentSource, 'requestPageApiAction');
|
||||
const remote = extractFunction(contentSource, 'applyServerMediaAction');
|
||||
expect(seekVideo.indexOf('pageApiActionRequired(EVENTS.SEEK)'))
|
||||
.toBeLessThan(seekVideo.indexOf('video.currentTime ='));
|
||||
expect(seekVideo).toContain('return requestPageApiAction(EVENTS.SEEK, targetTime)');
|
||||
expect(request).toContain('refusing unsafe native fallback');
|
||||
expect(remote.indexOf('if (!applied)')).toBeLessThan(remote.indexOf("type: 'CMD_ACK'"));
|
||||
expect(backgroundSource).toContain('target: { tabId },\n world: \'MAIN\'');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user