mirror of
https://github.com/iv-org/invidious.git
synced 2026-10-25 14:29:16 -05:00
Refresh navigation, search results, login, channel cards, and the watch page while keeping native no-JavaScript fallbacks. Add scoped player gestures, theater mode, accessible action menus, theme controls, and focused regression coverage. Model: OpenAI gpt-5.6-sol (high reasoning) Harness: T3 Code Codex
122 lines
8.2 KiB
JavaScript
122 lines
8.2 KiB
JavaScript
const { test } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const install = require('../assets/js/player-gestures.js');
|
||
|
||
class Target {
|
||
constructor() { this.listeners = {}; this.hidden = false; }
|
||
addEventListener(type, fn) { (this.listeners[type] ||= []).push(fn); }
|
||
removeEventListener(type, fn) { this.listeners[type] = (this.listeners[type] || []).filter(f => f !== fn); }
|
||
emit(type, extra = {}) {
|
||
const e = { target: surface, button: 0, pointerType: 'mouse', pointerId: 1,
|
||
clientX: 10, clientY: 10, preventDefault() { this.prevented = true; },
|
||
stopImmediatePropagation() { this.stopped = true; }, ...extra };
|
||
for (const fn of this.listeners[type] || []) fn(e);
|
||
return e;
|
||
}
|
||
}
|
||
const surface = { closest: () => null };
|
||
const control = { closest: selector => ['.vjs-play-control, .vjs-big-play-button', '.vjs-control-bar'].includes(selector) ? null : ({}) };
|
||
function setup(paused = false, speed = 1.75) {
|
||
const win = new Target(), doc = new Target(), el = new Target();
|
||
el.contains = target => target === surface;
|
||
doc.activeElement = surface;
|
||
el.focus = () => { doc.activeElement = surface; };
|
||
const tasks = new Map(); let next = 0, time = 40, indicator = false;
|
||
const player = { el: () => el, paused: () => paused,
|
||
playbackRate(v) { if (v !== undefined) speed = v; return speed; },
|
||
currentTime(v) { if (v !== undefined) time = v; return time; }, duration: () => 100,
|
||
play() { paused = false; return Promise.resolve(); }, pause() { paused = true; },
|
||
controls: () => true };
|
||
const gesture = install(player, { window: win, document: doc,
|
||
setTimeout(fn) { const id = ++next; tasks.set(id, fn); return id; },
|
||
clearTimeout(id) { tasks.delete(id); }, onHold(active) { indicator = active; } });
|
||
return { win, doc, el, player, gesture, indicator: () => indicator,
|
||
advance() { const callbacks = [...tasks.values()]; tasks.clear(); callbacks.forEach(fn => fn()); } };
|
||
}
|
||
test('Space tap toggles once on release, including repeated keydown', () => {
|
||
const h = setup(); h.win.emit('keydown', {key:' '}); h.win.emit('keydown', {key:' ',repeat:true});
|
||
assert.equal(h.player.paused(), false); h.win.emit('keyup',{key:' '}); assert.equal(h.player.paused(),true);
|
||
});
|
||
test('Space hold temporarily plays at 2× and restores 1.75× without toggling', () => {
|
||
const h=setup(); h.win.emit('keydown',{key:' '});h.advance();assert.equal(h.player.playbackRate(),2);assert.equal(h.indicator(),true);
|
||
h.win.emit('keyup',{key:' '});assert.equal(h.player.playbackRate(),1.75);assert.equal(h.player.paused(),false);assert.equal(h.indicator(),false);
|
||
});
|
||
test('hold from paused state restores paused state and original rate', () => {
|
||
const h=setup(true,.75);h.win.emit('keydown',{key:' '});h.advance();assert.equal(h.player.paused(),false);
|
||
h.win.emit('keyup',{key:' '});assert.equal(h.player.paused(),true);assert.equal(h.player.playbackRate(),.75);
|
||
});
|
||
test('mouse hold restores on release outside video and suppresses following click', () => {
|
||
const h=setup();h.el.emit('pointerdown');h.advance();assert.equal(h.player.playbackRate(),2);
|
||
h.win.emit('pointerup',{target:control});assert.equal(h.player.playbackRate(),1.75);
|
||
assert.equal(h.el.emit('click').prevented,true);
|
||
});
|
||
test('short mouse click is left to the native Video.js click behavior', () => {
|
||
const h=setup();h.el.emit('pointerdown');h.win.emit('pointerup');assert.equal(h.el.emit('click').prevented,undefined);assert.equal(h.player.paused(),false);
|
||
});
|
||
test('controls, editing, modifiers and touch do not start a hold', () => {
|
||
const h=setup();h.win.emit('keydown',{key:' ',target:control});h.win.emit('keydown',{key:' ',ctrlKey:true});h.el.emit('pointerdown',{target:control});h.el.emit('pointerdown',{pointerType:'touch'});h.advance();assert.equal(h.gesture.isHolding(),false);assert.equal(h.player.playbackRate(),1.75);
|
||
});
|
||
test('pointer movement cancels pending hold', () => {
|
||
const h=setup();h.el.emit('pointerdown');h.win.emit('pointermove',{clientX:50});h.advance();assert.equal(h.player.playbackRate(),1.75);
|
||
});
|
||
for(const event of ['blur','pointercancel']) test(event+' restores hold state',()=>{
|
||
const h=setup();h.el.emit('pointerdown');h.advance();h.win.emit(event);assert.equal(h.player.playbackRate(),1.75);assert.equal(h.gesture.isHolding(),false);
|
||
});
|
||
test('hidden document cancels Space hold without pausing playback',()=>{
|
||
const h=setup();h.win.emit('keydown',{key:' '});h.advance();h.doc.hidden=true;h.doc.emit('visibilitychange');assert.equal(h.player.playbackRate(),1.75);assert.equal(h.player.paused(),false);
|
||
});
|
||
test('J/L seek fixed ten seconds at 1.75× and clamp to duration',()=>{
|
||
const h=setup();h.win.emit('keydown',{key:'l'});assert.equal(h.player.currentTime(),50);h.win.emit('keydown',{key:'j'});assert.equal(h.player.currentTime(),40);h.player.currentTime(98);h.win.emit('keydown',{key:'l'});assert.equal(h.player.currentTime(),100);
|
||
});
|
||
test('K toggles once and held-key repeats are ignored',()=>{
|
||
const h=setup();h.win.emit('keydown',{key:'k'});h.win.emit('keydown',{key:'k',repeat:true});assert.equal(h.player.paused(),true);
|
||
});
|
||
test('overlapping inputs do not replace the original saved speed',()=>{
|
||
const h=setup();h.win.emit('keydown',{key:' '});h.advance();h.el.emit('pointerdown');h.win.emit('pointerup');assert.equal(h.player.playbackRate(),2);h.win.emit('keyup',{key:' '});assert.equal(h.player.playbackRate(),1.75);
|
||
});
|
||
test('dispose restores state and detaches handlers',()=>{
|
||
const h=setup();h.win.emit('keydown',{key:' '});h.advance();h.gesture.dispose();assert.equal(h.player.playbackRate(),1.75);h.win.emit('keydown',{key:'k'});assert.equal(h.player.paused(),false);
|
||
});
|
||
test('post-hold click remains suppressed across event-loop turns; next deliberate click works', () => {
|
||
const h=setup(true);h.el.emit('pointerdown');h.advance();h.win.emit('pointerup');
|
||
h.advance();assert.equal(h.el.emit('click',{detail:1}).prevented,true);
|
||
h.el.emit('pointerdown');h.win.emit('pointerup');assert.equal(h.el.emit('click',{detail:1}).prevented,undefined);
|
||
});
|
||
test('release outside does not suppress the next deliberate pointer or keyboard click', () => {
|
||
const h=setup();h.el.emit('pointerdown');h.advance();h.win.emit('pointerup',{target:control});
|
||
assert.equal(h.el.emit('click',{detail:0,target:control}).prevented,undefined);
|
||
h.el.emit('pointerdown');h.win.emit('pointerup');assert.equal(h.el.emit('click',{detail:1}).prevented,undefined);
|
||
});
|
||
test('held mouse consumes Video.js mouseup as well as the browser click', () => {
|
||
const h=setup(true);h.el.emit('pointerdown');h.advance();h.win.emit('pointerup');
|
||
assert.equal(h.el.emit('mouseup').prevented,true);
|
||
assert.equal(h.el.emit('click',{detail:1}).prevented,true);
|
||
});
|
||
|
||
test('shortcuts do not capture keys outside the player, even after playback starts', () => {
|
||
const h = setup(); h.doc.activeElement = control;
|
||
for (const key of ['j', 'k', 'l', ' ']) {
|
||
assert.equal(h.win.emit('keydown', {key, target: control}).prevented, undefined);
|
||
}
|
||
h.doc.activeElement = {};
|
||
assert.equal(h.win.emit('keydown', {key:'k'}).prevented, undefined);
|
||
assert.equal(h.player.paused(), false);
|
||
assert.equal(h.player.currentTime(), 40);
|
||
});
|
||
test('moving focus outside during Space hold restores speed without toggling', () => {
|
||
const h=setup(); h.win.emit('keydown',{key:' '}); h.advance();
|
||
h.doc.activeElement={}; h.doc.emit('focusin',{target:h.doc.activeElement});
|
||
assert.equal(h.player.playbackRate(),1.75);
|
||
assert.equal(h.player.paused(),false);
|
||
});
|
||
|
||
test('Play button focus accepts K/J/L and Space after clicking Play', () => {
|
||
const h=setup();
|
||
const button={closest: selector => selector === '.vjs-play-control, .vjs-big-play-button' ? button : control};
|
||
h.el.contains=target=>target===button;h.doc.activeElement=button;
|
||
h.win.emit('keydown',{key:'j',target:button});assert.equal(h.player.currentTime(),30);
|
||
h.win.emit('keydown',{key:'l',target:button});assert.equal(h.player.currentTime(),40);
|
||
h.win.emit('keydown',{key:'k',target:button});assert.equal(h.player.paused(),true);
|
||
h.win.emit('keydown',{key:' ',target:button});h.win.emit('keyup',{key:' ',target:button});assert.equal(h.player.paused(),false);
|
||
});
|