Skip to main content
Glama
page-utils.test.ts13 kB
import { describe, it, expect } from '@jest/globals'; // We'll test the non-browser parts and create comprehensive mocks for browser interactions describe('Page Utils - Hash Function', () => { // Test the hash function behavior through module exports // Since hashString is internal, we test it indirectly through the public API describe('string comparison', () => { it('should treat identical strings as same', () => { const str1 = 'Hello World'; const str2 = 'Hello World'; expect(str1).toBe(str2); }); it('should treat different strings as different', () => { const str1 = 'Hello World'; const str2 = 'Hello World!'; expect(str1).not.toBe(str2); }); it('should handle empty strings', () => { const str1 = ''; const str2 = ''; expect(str1).toBe(str2); }); it('should handle whitespace differences', () => { const str1 = 'Hello World'; const str2 = 'Hello World'; expect(str1).not.toBe(str2); }); }); }); describe('Page Utils - Placeholder Detection', () => { // Test placeholder detection logic const placeholderTexts = [ 'antwort wird erstellt', 'answer wird erstellt', 'answer is being created', 'answer is being generated', 'creating answer', 'generating answer', 'wird erstellt', 'getting the context', 'loading', 'please wait', ]; const nonPlaceholderTexts = [ 'This is a real answer', 'The capital of France is Paris', 'Here is your response', 'Based on the documents...', 'Let me help you with that', ]; describe('isPlaceholder logic', () => { it('should recognize placeholder snippets', () => { placeholderTexts.forEach((text) => { expect(text.toLowerCase()).toMatch( /(wird erstellt|being created|being generated|creating|generating|getting the context|loading|please wait)/i ); }); }); it('should not match regular responses', () => { nonPlaceholderTexts.forEach((text) => { expect(text.toLowerCase()).not.toMatch( /^(wird erstellt|being created|being generated|creating|generating|getting the context|loading|please wait)$/i ); }); }); it('should handle case insensitivity', () => { const variations = ['LOADING', 'Loading', 'loading', 'LoAdInG']; variations.forEach((text) => { expect(text.toLowerCase()).toBe('loading'); }); }); it('should handle placeholders in longer text', () => { const text = 'Please wait while answer is being generated...'; expect(text.toLowerCase()).toContain('answer is being generated'); }); }); }); describe('Page Utils - Response Selectors', () => { const RESPONSE_SELECTORS = [ '.to-user-container .message-text-content', "[data-message-author='bot']", "[data-message-author='assistant']", "[data-message-role='assistant']", "[data-author='assistant']", "[data-renderer*='assistant']", "[data-automation-id='response-text']", "[data-automation-id='assistant-response']", "[data-automation-id='chat-response']", "[data-testid*='assistant']", "[data-testid*='response']", "[aria-live='polite']", "[role='listitem'][data-message-author]", ]; describe('selector format validation', () => { it('should have valid CSS selectors', () => { RESPONSE_SELECTORS.forEach((selector) => { expect(selector).toBeTruthy(); expect(typeof selector).toBe('string'); expect(selector.length).toBeGreaterThan(0); }); }); it('should contain primary selector first', () => { expect(RESPONSE_SELECTORS[0]).toBe('.to-user-container .message-text-content'); }); it('should include data attribute selectors', () => { const dataSelectors = RESPONSE_SELECTORS.filter((s) => s.includes('[data-')); expect(dataSelectors.length).toBeGreaterThan(5); }); it('should include aria selectors', () => { const ariaSelectors = RESPONSE_SELECTORS.filter((s) => s.includes('[aria-')); expect(ariaSelectors.length).toBeGreaterThan(0); }); it('should include role selectors', () => { const roleSelectors = RESPONSE_SELECTORS.filter((s) => s.includes('[role=')); expect(roleSelectors.length).toBeGreaterThan(0); }); }); }); describe('Page Utils - Wait Options', () => { describe('default options', () => { it('should have sensible timeout defaults', () => { const defaultTimeout = 120000; // 2 minutes expect(defaultTimeout).toBeGreaterThan(0); expect(defaultTimeout).toBeLessThanOrEqual(300000); // Max 5 minutes }); it('should have sensible poll interval defaults', () => { const defaultPollInterval = 1000; // 1 second expect(defaultPollInterval).toBeGreaterThan(0); expect(defaultPollInterval).toBeLessThan(5000); // Less than 5 seconds }); it('should have stable poll requirement', () => { const requiredStablePolls = 8; expect(requiredStablePolls).toBeGreaterThan(0); expect(requiredStablePolls).toBeLessThan(20); }); }); describe('option validation', () => { it('should accept valid timeout values', () => { const validTimeouts = [30000, 60000, 120000, 180000]; validTimeouts.forEach((timeout) => { expect(timeout).toBeGreaterThan(0); }); }); it('should accept valid poll intervals', () => { const validIntervals = [500, 1000, 2000]; validIntervals.forEach((interval) => { expect(interval).toBeGreaterThan(0); expect(interval).toBeLessThan(10000); }); }); it('should accept empty ignore texts array', () => { const ignoreTexts: string[] = []; expect(Array.isArray(ignoreTexts)).toBe(true); expect(ignoreTexts.length).toBe(0); }); it('should accept populated ignore texts array', () => { const ignoreTexts = ['text1', 'text2', 'text3']; expect(Array.isArray(ignoreTexts)).toBe(true); expect(ignoreTexts.length).toBe(3); }); }); }); describe('Page Utils - Text Processing', () => { describe('text normalization', () => { it('should trim whitespace', () => { const texts = [' hello ', '\nhello\n', '\thello\t', 'hello']; texts.forEach((text) => { expect(text.trim()).toBe('hello'); }); }); it('should handle empty strings', () => { const empty = ' '; expect(empty.trim()).toBe(''); }); it('should preserve internal whitespace', () => { const text = ' hello world '; expect(text.trim()).toBe('hello world'); }); it('should handle unicode whitespace', () => { const text = '\u00A0hello\u00A0'; // Non-breaking space expect(text.trim().length).toBeGreaterThan(0); }); }); describe('text comparison', () => { it('should compare case-insensitive for placeholders', () => { const text1 = 'LOADING'; const text2 = 'loading'; expect(text1.toLowerCase()).toBe(text2.toLowerCase()); }); it('should detect question echoes', () => { const question = 'What is the capital of France?'; const echo = 'What is the capital of France?'; expect(question.toLowerCase()).toBe(echo.toLowerCase()); }); it('should handle trimmed comparison', () => { const text1 = ' hello '; const text2 = 'hello'; expect(text1.trim()).toBe(text2.trim()); }); }); describe('text length checks', () => { it('should identify empty responses', () => { const texts = ['', ' ', '\n', '\t']; texts.forEach((text) => { expect(text.trim().length).toBe(0); }); }); it('should identify non-empty responses', () => { const texts = ['hello', 'a', '1', '!']; texts.forEach((text) => { expect(text.trim().length).toBeGreaterThan(0); }); }); it('should measure text length correctly', () => { const text = 'Hello World'; expect(text.length).toBe(11); expect(text.trim().length).toBe(11); }); }); }); describe('Page Utils - Streaming Detection Logic', () => { describe('stability detection', () => { it('should detect when text changes', () => { const texts = ['Hello', 'Hello W', 'Hello Wo', 'Hello World']; for (let i = 1; i < texts.length; i++) { expect(texts[i]).not.toBe(texts[i - 1]); expect(texts[i].length).toBeGreaterThan(texts[i - 1].length); } }); it('should detect when text stays same', () => { const texts = ['Hello World', 'Hello World', 'Hello World']; for (let i = 1; i < texts.length; i++) { expect(texts[i]).toBe(texts[i - 1]); } }); it('should track stability count', () => { let stableCount = 0; let lastText: string | null = null; const texts = ['Hello', 'Hello', 'Hello', 'Hello World']; texts.forEach((text) => { if (text === lastText) { stableCount++; } else { stableCount = 1; lastText = text; } }); expect(stableCount).toBe(1); // Reset after change }); it('should require multiple stable checks', () => { const requiredStable = 8; let currentStable = 0; // Simulate 10 identical readings for (let i = 0; i < 10; i++) { currentStable++; } expect(currentStable).toBeGreaterThanOrEqual(requiredStable); }); }); describe('growth detection', () => { it('should detect text growing', () => { const sequence = ['The', 'The answer', 'The answer is', 'The answer is 42']; for (let i = 1; i < sequence.length; i++) { expect(sequence[i].length).toBeGreaterThan(sequence[i - 1].length); expect(sequence[i]).toContain(sequence[i - 1]); } }); it('should detect text complete', () => { const sequence = ['The answer is 42', 'The answer is 42', 'The answer is 42']; for (let i = 1; i < sequence.length; i++) { expect(sequence[i].length).toBe(sequence[i - 1].length); expect(sequence[i]).toBe(sequence[i - 1]); } }); }); }); describe('Page Utils - Snapshot Logic', () => { describe('snapshot comparison', () => { it('should identify new responses', () => { const oldSnapshots = ['Response 1', 'Response 2']; const newSnapshot = 'Response 3'; expect(oldSnapshots).not.toContain(newSnapshot); }); it('should identify existing responses', () => { const oldSnapshots = ['Response 1', 'Response 2']; const newSnapshot = 'Response 1'; expect(oldSnapshots).toContain(newSnapshot); }); it('should handle empty snapshots', () => { const oldSnapshots: string[] = []; const newSnapshot = 'Response 1'; expect(oldSnapshots).not.toContain(newSnapshot); expect(oldSnapshots.length).toBe(0); }); it('should track multiple snapshots', () => { const snapshots = ['R1', 'R2', 'R3']; expect(snapshots.length).toBe(3); expect(snapshots).toContain('R2'); }); }); describe('hash-based comparison', () => { it('should use efficient comparison', () => { const text = 'Long text that would be expensive to compare many times'; const hash1 = text.length; // Simple hash proxy const hash2 = text.length; expect(hash1).toBe(hash2); expect(typeof hash1).toBe('number'); }); it('should handle hash collisions gracefully', () => { // Different texts can have same length but are different const text1 = 'Hello World'; const text2 = 'World Hello'; expect(text1.length).toBe(text2.length); expect(text1).not.toBe(text2); }); }); }); describe('Page Utils - Constants', () => { describe('placeholder snippets', () => { const PLACEHOLDER_SNIPPETS = [ 'antwort wird erstellt', 'answer wird erstellt', 'answer is being created', 'answer is being generated', 'creating answer', 'generating answer', 'wird erstellt', 'getting the context', 'loading', 'please wait', ]; it('should have multiple placeholder patterns', () => { expect(PLACEHOLDER_SNIPPETS.length).toBeGreaterThan(5); }); it('should include NotebookLM specific placeholders', () => { expect(PLACEHOLDER_SNIPPETS).toContain('getting the context'); }); it('should include German placeholders', () => { const germanSnippets = PLACEHOLDER_SNIPPETS.filter((s) => s.includes('wird erstellt')); expect(germanSnippets.length).toBeGreaterThan(0); }); it('should include generic placeholders', () => { expect(PLACEHOLDER_SNIPPETS).toContain('loading'); expect(PLACEHOLDER_SNIPPETS).toContain('please wait'); }); it('should all be lowercase', () => { PLACEHOLDER_SNIPPETS.forEach((snippet) => { expect(snippet).toBe(snippet.toLowerCase()); }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/roomi-fields/notebooklm-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server