test: harden release and browser gates

This commit is contained in:
KoalaDev
2026-08-21 15:49:51 +02:00
parent 230e7f5932
commit 7286a6db3d
32 changed files with 1150 additions and 201 deletions
+42 -1
View File
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
HOST_ACCESS_REQUIRED_STATUS,
addTabHostAccessRequest,
@@ -16,6 +16,8 @@ import {
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
describe('host access helpers', () => {
afterEach(() => vi.useRealTimers());
it('normalizes only positive safe tab IDs', () => {
expect(HOST_ACCESS_REQUIRED_STATUS).toBe('host_permission_required');
for (const invalid of [null, undefined, '', 0, true, [42], '42.5', Number.MAX_SAFE_INTEGER + 1]) {
@@ -38,6 +40,11 @@ describe('host access helpers', () => {
});
expect(describeTabUrl('chrome://extensions/')).toBeNull();
expect(describeTabUrl('not a url')).toBeNull();
expect(describeTabUrl('file:///Users/koala/movie.mp4')).toEqual({
url: 'file:///Users/koala/movie.mp4',
host: 'local file',
originPattern: 'file:///*'
});
});
it('checks the selected tab origin and preserves an unknown callback result', async () => {
@@ -112,9 +119,43 @@ describe('host access helpers', () => {
};
await expect(requestOriginPermission(callbackChrome, 'https://video.example/*')).resolves.toBe(true);
await expect(requestOriginPermission({ permissions: {} }, 'https://video.example/*')).resolves.toBeNull();
await expect(requestOriginPermission(callbackChrome, '')).resolves.toBeNull();
await expect(requestOriginPermission({
permissions: { request: async () => { throw new Error('denied'); } }
}, 'https://video.example/*')).resolves.toBe(false);
await expect(addTabHostAccessRequest({
permissions: { addHostAccessRequest: async () => { throw new Error('denied'); } }
}, 42)).resolves.toBe(false);
await expect(removeTabHostAccessRequest({
permissions: { removeHostAccessRequest: async () => { throw new Error('denied'); } }
}, 42)).resolves.toBe(false);
expect(isHostAccessError(new Error('Missing host permission for the tab'))).toBe(true);
expect(isHostAccessError(new Error('No tab with id: 42'))).toBe(false);
});
it('treats permission inspection errors and timeouts as advisory unknowns', async () => {
const base = {
tabs: { get: async () => ({ url: 'https://video.example/watch' }) }
};
await expect(inspectTabHostAccess({
...base,
permissions: { contains: async () => { throw new Error('permission API failed'); } }
}, 42)).resolves.toMatchObject({ granted: null });
await expect(inspectTabHostAccess({
...base,
runtime: { lastError: { message: 'permission callback failed' } },
permissions: { contains: (_request, callback) => callback(false) }
}, 42)).resolves.toMatchObject({ granted: null });
vi.useFakeTimers();
const pending = inspectTabHostAccess({
...base,
permissions: { contains: () => undefined }
}, 42);
await vi.advanceTimersByTimeAsync(1000);
await expect(pending).resolves.toMatchObject({ granted: null });
});
});
describe('host access recovery contracts', () => {
+11 -11
View File
@@ -1413,16 +1413,16 @@
</a>
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="tab-room" data-i18n="TAB_ROOM" data-i18n-title="TAB_ROOM_TOOLTIP" title="Room settings and connection">Room</button>
<button class="tab-btn" data-tab="tab-sync" data-i18n="TAB_SYNC" data-i18n-title="TAB_SYNC_TOOLTIP" title="Video sync controls and remote actions">Sync</button>
<button class="tab-btn" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP" title="Extension preferences">Settings</button>
<button class="tab-btn" data-tab="tab-dev" data-i18n="TAB_STATUS" data-i18n-title="TAB_STATUS_TOOLTIP" title="Advanced Diagnostics & Logs">Status</button>
<button id="devToolsTabBtn" class="tab-btn" data-tab="tab-devtools" style="display:none;">Dev</button>
<div class="tabs" role="tablist" aria-label="KoalaSync sections">
<button id="tab-room-button" class="tab-btn active" role="tab" aria-selected="true" aria-controls="tab-room" data-tab="tab-room" data-i18n="TAB_ROOM" data-i18n-title="TAB_ROOM_TOOLTIP" title="Room settings and connection">Room</button>
<button id="tab-sync-button" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-sync" tabindex="-1" data-tab="tab-sync" data-i18n="TAB_SYNC" data-i18n-title="TAB_SYNC_TOOLTIP" title="Video sync controls and remote actions">Sync</button>
<button id="tab-settings-button" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-settings" tabindex="-1" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP" title="Extension preferences">Settings</button>
<button id="tab-dev-button" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-dev" tabindex="-1" data-tab="tab-dev" data-i18n="TAB_STATUS" data-i18n-title="TAB_STATUS_TOOLTIP" title="Advanced Diagnostics & Logs">Status</button>
<button id="devToolsTabBtn" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-devtools" tabindex="-1" data-tab="tab-devtools" style="display:none;">Dev</button>
</div>
<!-- Room Tab -->
<div id="tab-room" class="tab-content active">
<div id="tab-room" class="tab-content active" role="tabpanel" aria-labelledby="tab-room-button">
<!-- JOIN SECTION: Visible when not in a room -->
<div id="section-join">
@@ -1510,7 +1510,7 @@
</div>
<!-- Sync Tab -->
<div id="tab-sync" class="tab-content">
<div id="tab-sync" class="tab-content" role="tabpanel" aria-labelledby="tab-sync-button" aria-hidden="true">
<!-- SYNC ACTIVE: Visible when in a room -->
<div id="sync-active">
<div class="form-group" style="position: relative;">
@@ -1576,7 +1576,7 @@
</div>
<!-- Settings Tab -->
<div id="tab-settings" class="tab-content">
<div id="tab-settings" class="tab-content" role="tabpanel" aria-labelledby="tab-settings-button" aria-hidden="true">
<details class="form-group" name="settings-accordion" open>
<summary title="Change your username, theme, and language." data-i18n="LABEL_SETTINGS_GROUP_PROFILE" data-i18n-title="LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP">Profile &amp; Appearance</summary>
<div class="details-content">
@@ -1813,7 +1813,7 @@
</div>
<!-- Dev Tab -->
<div id="tab-dev" class="tab-content">
<div id="tab-dev" class="tab-content" role="tabpanel" aria-labelledby="tab-dev-button" aria-hidden="true">
<label title="Current WebSocket connection state" data-i18n="LABEL_CONN_STATUS" data-i18n-title="LABEL_CONN_STATUS_TOOLTIP">Connection Status</label>
<div id="connStatus" class="info-card" style="display:flex; align-items:center; gap: 10px;">
<span id="connDot" class="status-dot status-offline"></span>
@@ -1845,7 +1845,7 @@
<div id="logList"></div>
</div>
<div id="tab-devtools" class="tab-content">
<div id="tab-devtools" class="tab-content" role="tabpanel" aria-labelledby="devToolsTabBtn" aria-hidden="true">
<label>Remote Seek</label>
<div class="info-card" style="display:flex; gap:8px; margin-bottom:15px;">
<button id="remoteSeekBack" class="secondary" style="flex:1; font-size:12px;">-30s</button>
+26 -2
View File
@@ -1791,12 +1791,22 @@ elements.serverUrl.addEventListener('change', () => {
elements.tabs.forEach(btn => {
btn.addEventListener('click', () => {
elements.tabs.forEach(b => b.classList.remove('active'));
elements.contents.forEach(c => c.classList.remove('active'));
elements.tabs.forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
b.tabIndex = -1;
});
elements.contents.forEach(c => {
c.classList.remove('active');
c.setAttribute('aria-hidden', 'true');
});
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
btn.tabIndex = 0;
const targetContent = document.getElementById(btn.dataset.tab);
targetContent.classList.add('active');
targetContent.removeAttribute('aria-hidden');
targetContent.classList.remove('tab-active-animate');
void targetContent.offsetWidth; // Force reflow to restart animation
@@ -1808,6 +1818,20 @@ elements.tabs.forEach(btn => {
chrome.storage.local.set({ activeTab: btn.dataset.tab });
});
btn.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
const visibleTabs = [...elements.tabs].filter(tab => window.getComputedStyle(tab).display !== 'none');
const currentIndex = visibleTabs.indexOf(btn);
const nextIndex = event.key === 'Home'
? 0
: event.key === 'End'
? visibleTabs.length - 1
: (currentIndex + (event.key === 'ArrowRight' ? 1 : -1) + visibleTabs.length) % visibleTabs.length;
event.preventDefault();
visibleTabs[nextIndex].focus();
visibleTabs[nextIndex].click();
});
});
function showToast(message, type = 'info', duration = 3000) {
+7
View File
@@ -21,13 +21,20 @@ describe('title privacy', () => {
expect(normalizeTabTitle('(12) Testvideo - YouTube')).toBe('Testvideo - YouTube');
expect(normalizeTabTitle('[999+] Testvideo - YouTube')).toBe('Testvideo - YouTube');
expect(normalizeTabTitle('(500) Days of Summer')).toBe('Days of Summer');
for (const title of ['[7] Testvideo', '(99+) Testvideo', '(999+) Testvideo', '(101) Testvideo', '[101] Testvideo']) {
expect(normalizeTabTitle(title)).toBe('Testvideo');
}
expect(normalizeTabTitle(null)).toBeNull();
expect(normalizeTabTitle(' ')).toBeNull();
expect(sanitizeTabTitle('', true)).toBeNull();
});
it('keeps tab-title and media-title privacy independent', () => {
expect(sanitizeTabTitle('(12) Private Tab', true)).toBe('Private Tab');
expect(sanitizeTabTitle('Private Tab', false)).toBeNull();
expect(sanitizeSharedTitle('Example Movie', 'full')).toBe('Example Movie');
expect(sanitizeSharedTitle('', 'full')).toBeNull();
expect(sanitizeSharedTitle(null, 'full')).toBeNull();
expect(sanitizeSharedTitle('Show Name - S01/E04 - Title', 'episode')).toBe('S01E04');
expect(sanitizeSharedTitle('Folge 7 - Private Server', 'episode')).toBe('EP007');
expect(sanitizeSharedTitle('Example Movie', 'episode')).toBeNull();