Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
833 changes: 367 additions & 466 deletions app/analyze/[videoId]/page.tsx

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions app/analyze/__tests__/cached-highlights-flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import test from 'node:test';

const pageSource = readFileSync(
join(process.cwd(), 'app/analyze/[videoId]/page.tsx'),
'utf8'
);

function getHandleGenerateHighlightsSource(): string {
const start = pageSource.indexOf('const handleGenerateHighlights = useCallback');
assert.notEqual(start, -1, 'Expected handleGenerateHighlights callback to exist');

const end = pageSource.indexOf('const handleThemeSelect = useCallback', start);
assert.notEqual(end, -1, 'Expected handleThemeSelect callback after handleGenerateHighlights');

return pageSource.slice(start, end);
}

test('cached highlight payload is stored without displaying topics on cached load', () => {
assert.match(pageSource, /const cachedHighlightPayloadRef = useRef<CachedHighlightPayload \| null>\(null\)/);
assert.doesNotMatch(pageSource, /const \[cachedHighlightPayload, setCachedHighlightPayload\] = useState/);

const cachedLoadStart = pageSource.indexOf('if (cacheData.cached)');
assert.notEqual(cachedLoadStart, -1, 'Expected cached-video load branch to exist');

const cachedLoadSource = pageSource.slice(cachedLoadStart, cachedLoadStart + 8000);
assert.match(cachedLoadSource, /cachedHighlightPayloadRef\.current =/);

const storeIndex = cachedLoadSource.indexOf('cachedHighlightPayloadRef.current =');
const returnIndex = cachedLoadSource.indexOf('return; // Exit early - no need to fetch anything else');
assert.ok(storeIndex > -1 && returnIndex > -1 && storeIndex < returnIndex);

const beforeReturnSource = cachedLoadSource.slice(0, returnIndex);
assert.doesNotMatch(beforeReturnSource, /setTopics\(cacheData\.topics\)/);
assert.doesNotMatch(beforeReturnSource, /setBaseTopics\(cacheData\.topics\)/);
});

test('generate highlights reveals cached topics locally before calling video-analysis', () => {
const handleSource = getHandleGenerateHighlightsSource();

assert.match(handleSource, /const cachedHighlightPayload = cachedHighlightPayloadRef\.current/);
assert.match(handleSource, /applyHighlightResponse\(cachedHighlightPayload/);
assert.match(handleSource, /cachedHighlightPayloadRef\.current = null/);

const cachedRevealIndex = handleSource.indexOf('applyHighlightResponse(cachedHighlightPayload');
const fetchIndex = handleSource.indexOf('fetch("/api/video-analysis"');
assert.ok(cachedRevealIndex > -1 && fetchIndex > -1 && cachedRevealIndex < fetchIndex);
});

test('cached reveal skips suggested question generation when cached questions exist', () => {
const handleSource = getHandleGenerateHighlightsSource();
const cachedRevealIndex = handleSource.indexOf('applyHighlightResponse(cachedHighlightPayload');
assert.notEqual(cachedRevealIndex, -1, 'Expected cached reveal branch to apply cached highlights');

const cachedRevealSource = handleSource.slice(cachedRevealIndex, handleSource.indexOf('const requestKey', cachedRevealIndex));
assert.match(cachedRevealSource, /cachedSuggestedQuestions\?\.length/);
assert.match(cachedRevealSource, /generateSuggestedQuestionsForTopics\(generatedTopics, transcript, videoInfo\)/);

const guardIndex = cachedRevealSource.indexOf('cachedSuggestedQuestions?.length');
const generateIndex = cachedRevealSource.indexOf('generateSuggestedQuestionsForTopics(generatedTopics, transcript, videoInfo)');
assert.ok(guardIndex > -1 && generateIndex > -1 && guardIndex < generateIndex);
});
45 changes: 45 additions & 0 deletions app/analyze/__tests__/right-column-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import test from 'node:test';

const pageSource = readFileSync(
join(process.cwd(), 'app/analyze/[videoId]/page.tsx'),
'utf8'
);

test('right column height recalculates when transcript workspace becomes visible', () => {
const effectStart = pageSource.indexOf('// Dynamically adjust right column height');
assert.notEqual(effectStart, -1, 'Expected right-column height effect to exist');

const effectSource = pageSource.slice(effectStart, effectStart + 1800);

assert.match(effectSource, /requestAnimationFrame/);
assert.match(effectSource, /transcript\.length/);
assert.match(effectSource, /pageState/);
});

test('right column has a defensive minimum height before measurement completes', () => {
const containerStart = pageSource.indexOf('id="right-column-container"');
assert.notEqual(containerStart, -1, 'Expected right-column container to exist');

const containerSource = pageSource.slice(containerStart, containerStart + 500);
assert.match(containerSource, /minHeight:\s*420/);
});

test('youtube player remounts when the analyzed video changes', () => {
const playerStart = pageSource.indexOf('<YouTubePlayer\n');
assert.notEqual(playerStart, -1, 'Expected YouTubePlayer render to exist');

const playerSource = pageSource.slice(playerStart, playerStart + 600);
assert.match(playerSource, /key=\{videoId\}/);
});

test('transcript seeks try the current youtube player before queuing a command', () => {
const requestSeekStart = pageSource.indexOf('const requestSeek = useCallback');
assert.notEqual(requestSeekStart, -1, 'Expected requestSeek callback to exist');

const requestSeekSource = pageSource.slice(requestSeekStart, requestSeekStart + 300);
assert.match(requestSeekSource, /youtubePlayerRef\.current\?\.seekTo\(time\)/);
assert.match(requestSeekSource, /setPlaybackCommand\(\{ type: 'SEEK', time \}\)/);
});
54 changes: 54 additions & 0 deletions app/analyze/__tests__/summary-background-flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import test from 'node:test';

const pageSource = readFileSync(
join(process.cwd(), 'app/analyze/[videoId]/page.tsx'),
'utf8'
);

function getOperationSource(operationName: string): string {
const marker = `'${operationName}'`;
const start = pageSource.indexOf(marker);
assert.notEqual(start, -1, `Expected to find background operation ${operationName}`);

return pageSource.slice(start, start + 3500);
}

function getSummaryFailureBranch(operationName: string): string {
const operation = getOperationSource(operationName);
const start = operation.indexOf('if (!summaryRes.ok)');
assert.notEqual(start, -1, `Expected ${operationName} to guard failed summary responses`);

let depth = 0;
for (let index = start; index < operation.length; index += 1) {
const char = operation[index];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return operation.slice(start, index + 1);
}
}
}

assert.fail(`Expected to find end of failed summary branch in ${operationName}`);
}

test('cached summary failure stays local to takeaways state', () => {
const failureBranch = getSummaryFailureBranch('generate-cached-takeaways');

assert.match(failureBranch, /setTakeawaysError\(/);
assert.match(failureBranch, /return null;/);
assert.doesNotMatch(failureBranch, /throw new Error/);
});

test('new-video summary failure stays local to takeaways state', () => {
const failureBranch = getSummaryFailureBranch('generate-takeaways');

assert.match(failureBranch, /setTakeawaysError\(/);
assert.match(failureBranch, /return null;/);
assert.doesNotMatch(failureBranch, /throw new Error/);
});
75 changes: 75 additions & 0 deletions components/__tests__/highlights-panel.generate.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';

import { HighlightsPanel } from '@/components/highlights-panel';

const noop = () => {};

test('HighlightsPanel shows generate CTA instead of Play All before reels exist', () => {
const markup = renderToStaticMarkup(
<HighlightsPanel
topics={[]}
selectedTopic={null}
onTopicSelect={noop}
onSeek={noop}
onPlayAll={noop}
isPlayingAll={false}
currentTime={0}
videoDuration={120}
onGenerateHighlights={noop}
/>
);

assert.match(markup, /Generate highlight reels/);
assert.doesNotMatch(markup, />Play All</);
});

test('HighlightsPanel shows elapsed time while generating highlight reels', () => {
const markup = renderToStaticMarkup(
<HighlightsPanel
topics={[]}
selectedTopic={null}
onTopicSelect={noop}
onSeek={noop}
onPlayAll={noop}
isPlayingAll={false}
currentTime={0}
videoDuration={120}
onGenerateHighlights={noop}
isGeneratingHighlights
highlightGenerationElapsedTime={19}
/>
);

assert.match(markup, /Analyzing video and generating highlight reels/);
assert.match(markup, /Creating highlight reels\.\.\. \(19 seconds\)/);
});

test('HighlightsPanel keeps Play All controls when reels exist', () => {
const markup = renderToStaticMarkup(
<HighlightsPanel
topics={[
{
id: 'topic-1',
title: 'Useful section',
description: 'A generated reel',
duration: 30,
segments: [{ start: 10, end: 40, text: 'Useful section text' }],
},
]}
selectedTopic={null}
onTopicSelect={noop}
onSeek={noop}
onPlayAll={noop}
isPlayingAll={false}
currentTime={0}
videoDuration={120}
onGenerateHighlights={noop}
/>
);

assert.match(markup, />Play All</);
assert.doesNotMatch(markup, /Generate highlight reels/);
});
106 changes: 106 additions & 0 deletions components/__tests__/youtube-player.timeline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

import {
canExecutePlaybackCommand,
getYouTubePlayerElementId,
getYouTubePlayerVars,
seekPlayerTo,
shouldPollPlayerTime,
shouldQueuePlaybackCommand,
shouldRenderHighlightTimeline,
} from '@/components/youtube-player';
import { normalizeDensityBuckets } from '@/components/video-progress-bar';

const youtubePlayerSource = readFileSync(
join(process.cwd(), 'components/youtube-player.tsx'),
'utf8'
);

test('shouldRenderHighlightTimeline hides the custom bar until reels exist', () => {
assert.equal(shouldRenderHighlightTimeline(300, 0), false);
assert.equal(shouldRenderHighlightTimeline(0, 3), false);
assert.equal(shouldRenderHighlightTimeline(300, 3), true);
});

test('getYouTubePlayerVars includes origin when available', () => {
assert.deepEqual(getYouTubePlayerVars('http://localhost:3001'), {
autoplay: 0,
controls: 1,
modestbranding: 1,
rel: 0,
origin: 'http://localhost:3001',
});
});

test('getYouTubePlayerElementId scopes the iframe container to the video', () => {
assert.equal(getYouTubePlayerElementId('abc123'), 'youtube-player-abc123');
assert.equal(getYouTubePlayerElementId('video_with_symbols'), 'youtube-player-video_with_symbols');
});

test('canExecutePlaybackCommand waits for player readiness', () => {
assert.equal(canExecutePlaybackCommand(null, false, { type: 'SEEK', time: 10 }), false);
assert.equal(canExecutePlaybackCommand({ seekTo: () => {} }, false, { type: 'SEEK', time: 10 }), false);
assert.equal(canExecutePlaybackCommand({ playVideo: () => {} }, true, { type: 'SEEK', time: 10 }), false);
assert.equal(canExecutePlaybackCommand({ seekTo: () => {} }, true, { type: 'SEEK', time: 10 }), true);
assert.equal(canExecutePlaybackCommand({ playVideo: () => {} }, true, { type: 'PLAY' }), true);
});

test('shouldPollPlayerTime waits for a ready player time API', () => {
assert.equal(shouldPollPlayerTime(null, false), false);
assert.equal(shouldPollPlayerTime({ getCurrentTime: () => 12 }, false), false);
assert.equal(shouldPollPlayerTime({ seekTo: () => {} }, true), false);
assert.equal(shouldPollPlayerTime({ getCurrentTime: () => 12 }, true), true);
});

test('shouldQueuePlaybackCommand keeps commands until player is ready', () => {
assert.equal(shouldQueuePlaybackCommand(null, false, null), false);
assert.equal(shouldQueuePlaybackCommand({ type: 'SEEK', time: 25 }, false, null), true);
assert.equal(shouldQueuePlaybackCommand({ type: 'SEEK', time: 25 }, false, { seekTo: () => {} }), true);
assert.equal(shouldQueuePlaybackCommand({ type: 'SEEK', time: 25 }, true, { playVideo: () => {} }), true);
assert.equal(shouldQueuePlaybackCommand({ type: 'SEEK', time: 25 }, true, { seekTo: () => {} }), false);
});

test('seekPlayerTo reports whether direct seeking was available', () => {
const calls: Array<[number, boolean]> = [];
const syncCalls: number[] = [];
const player = {
seekTo: (time: number, allowSeekAhead: boolean) => calls.push([time, allowSeekAhead]),
playVideo: () => {},
};

assert.equal(seekPlayerTo(null, false, 15, (time) => syncCalls.push(time)), false);
assert.equal(seekPlayerTo({ playVideo: () => {} }, true, 15, (time) => syncCalls.push(time)), false);
assert.equal(seekPlayerTo(player, true, 42, (time) => syncCalls.push(time)), true);
assert.deepEqual(calls, [[42, true]]);
assert.deepEqual(syncCalls, [42]);
});

test('youtube player stores iframe readiness in a ref for direct seeking', () => {
assert.match(youtubePlayerSource, /const playerReadyRef = useRef\(false\)/);
assert.match(youtubePlayerSource, /playerReadyRef\.current = true/);
assert.match(youtubePlayerSource, /playerReadyRef\.current = false/);
assert.match(youtubePlayerSource, /seekPlayerTo\(playerRef\.current, playerReadyRef\.current, time, syncSeekTime\)/);
});

test('youtube player stores the constructed iframe player before onReady', () => {
assert.match(youtubePlayerSource, /const nextPlayer = new \(window as any\)\.YT\.Player/);
assert.match(youtubePlayerSource, /playerRef\.current = nextPlayer/);
});

test('youtube player replays a queued command when iframe readiness fires', () => {
const readyHandlerStart = youtubePlayerSource.indexOf('onReady:');
assert.notEqual(readyHandlerStart, -1, 'Expected YouTube onReady handler to exist');

const readyHandlerSource = youtubePlayerSource.slice(readyHandlerStart, readyHandlerStart + 1200);
assert.match(readyHandlerSource, /pendingPlaybackCommandRef\.current/);
assert.match(readyHandlerSource, /executePlaybackCommandRef\.current/);
assert.match(readyHandlerSource, /onCommandExecuted\?\.\(\)/);
});

test('normalizeDensityBuckets handles zero-density timelines', () => {
assert.deepEqual(normalizeDensityBuckets([0, 0, 0]), [0, 0, 0]);
assert.deepEqual(normalizeDensityBuckets([0, 2, 4]), [0, 0.5, 1]);
});
Loading
Loading