mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-29 12:07:09 +00:00
test: harden release and browser gates
This commit is contained in:
@@ -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
@@ -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 & 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
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user