Skip to main content
Glama

Lighthouse MCP

by mizchi
l2-score-analysis.test.tsโ€ข12.7 kB
/** * Unit tests for L2 Score Analysis Tool */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { executeL2ScoreAnalysis, l2ScoreAnalysisTool } from '../../../src/tools/l2-score-analysis'; import * as l1GetReport from '../../../src/tools/l1-get-report'; import * as l1Collect from '../../../src/tools/l1-collect-single'; import * as scores from '../../../src/analyzers/scores'; vi.mock('../../../src/tools/l1-get-report'); vi.mock('../../../src/tools/l1-collect-single'); vi.mock('../../../src/analyzers/scores'); describe('L2 Score Analysis Tool', () => { const mockReport = { categories: { performance: { score: 0.85, auditRefs: [], }, }, audits: {}, }; const mockScoreAnalysis = { categories: { performance: { score: 0.85, audits: [ { id: 'first-contentful-paint', title: 'First Contentful Paint', score: 0.9, weight: 0.1, weightedScore: 0.09, displayValue: '1.8 s', }, { id: 'largest-contentful-paint', title: 'Largest Contentful Paint', score: 0.75, weight: 0.25, weightedScore: 0.1875, displayValue: '2.5 s', }, { id: 'cumulative-layout-shift', title: 'Cumulative Layout Shift', score: 1, weight: 0.15, weightedScore: 0.15, displayValue: '0', }, { id: 'total-blocking-time', title: 'Total Blocking Time', score: 0.85, weight: 0.3, weightedScore: 0.255, displayValue: '300 ms', }, { id: 'speed-index', title: 'Speed Index', score: 0.88, weight: 0.1, weightedScore: 0.088, displayValue: '3.2 s', }, { id: 'time-to-interactive', title: 'Time to Interactive', score: 0.82, weight: 0.1, weightedScore: 0.082, displayValue: '3.8 s', }, ], }, accessibility: { score: 0.92, audits: [ { id: 'color-contrast', title: 'Color Contrast', score: 1, weight: 0.3, weightedScore: 0.3, displayValue: null, }, { id: 'image-alt', title: 'Image Alt Text', score: 0.9, weight: 0.2, weightedScore: 0.18, displayValue: '2 images missing alt text', }, ], }, }, }; beforeEach(() => { vi.clearAllMocks(); vi.spyOn(scores, 'analyzeReport').mockReturnValue(mockScoreAnalysis as any); }); describe('Tool Definition', () => { it('should have correct tool metadata', () => { expect(l2ScoreAnalysisTool.name).toBe('l2_score_analysis'); expect(l2ScoreAnalysisTool.description).toContain('score breakdown'); expect(l2ScoreAnalysisTool.description).toContain('Layer 2'); }); it('should define proper schema', () => { const props = l2ScoreAnalysisTool.inputSchema.properties as any; expect(props.reportId.type).toBe('string'); expect(props.url.type).toBe('string'); expect(props.device.enum).toContain('mobile'); expect(props.category.enum).toContain('performance'); expect(props.category.enum).toContain('accessibility'); expect(props.category.default).toBe('performance'); }); }); describe('executeL2ScoreAnalysis', () => { it('should analyze score breakdown from report ID', async () => { vi.spyOn(l1GetReport, 'executeL1GetReport').mockResolvedValue({ reportId: 'test-report', data: mockReport as any, metadata: { url: 'https://example.com', device: 'mobile', categories: ['performance'], timestamp: Date.now(), }, }); const result = await executeL2ScoreAnalysis({ reportId: 'test-report', category: 'performance', }); expect(result.reportId).toBe('test-report'); expect(result.scoreAnalysis.category).toBe('performance'); expect(result.scoreAnalysis.score).toBe(0.85); expect(result.scoreAnalysis.weightedMetrics).toHaveLength(6); const fcp = result.scoreAnalysis.weightedMetrics.find(m => m.id === 'first-contentful-paint'); expect(fcp).toEqual({ id: 'first-contentful-paint', title: 'First Contentful Paint', score: 0.9, weight: 0.1, contribution: 0.09, displayValue: '1.8 s', }); expect(result.scoreAnalysis.opportunities).toHaveLength(5); // All metrics with score < 1 (except CLS which is 1) expect(result.scoreAnalysis.diagnostics).toHaveLength(6); }); it('should collect and analyze by URL', async () => { vi.spyOn(l1Collect, 'executeL1Collect').mockResolvedValue({ reportId: 'new-report', url: 'https://example.com', device: 'desktop', categories: ['accessibility'], timestamp: Date.now(), cached: false, }); vi.spyOn(l1GetReport, 'executeL1GetReport').mockResolvedValue({ reportId: 'new-report', data: mockReport as any, metadata: { url: 'https://example.com', device: 'desktop', categories: ['accessibility'], timestamp: Date.now(), }, }); const result = await executeL2ScoreAnalysis({ url: 'https://example.com', device: 'desktop', category: 'accessibility', }); expect(l1Collect.executeL1Collect).toHaveBeenCalledWith({ url: 'https://example.com', device: 'desktop', categories: ['accessibility'], gather: false, }); expect(result.reportId).toBe('new-report'); expect(result.scoreAnalysis.category).toBe('accessibility'); }); it('should use performance as default category', async () => { vi.spyOn(l1GetReport, 'executeL1GetReport').mockResolvedValue({ reportId: 'test-report', data: mockReport as any, metadata: { url: 'https://example.com', device: 'mobile', categories: ['performance'], timestamp: Date.now(), }, }); const result = await executeL2ScoreAnalysis({ reportId: 'test-report', }); expect(result.scoreAnalysis.category).toBe('performance'); }); it('should throw error when neither reportId nor url provided', async () => { await expect(executeL2ScoreAnalysis({})).rejects.toThrow( 'Either reportId, url, or report is required' ); }); it('should throw error when category not found in report', async () => { vi.spyOn(l1GetReport, 'executeL1GetReport').mockResolvedValue({ reportId: 'test-report', data: mockReport as any, metadata: { url: 'https://example.com', device: 'mobile', categories: ['performance'], timestamp: Date.now(), }, }); vi.spyOn(scores, 'analyzeReport').mockReturnValue({ categories: {}, } as any); await expect(executeL2ScoreAnalysis({ reportId: 'test-report', category: 'performance', })).rejects.toThrow('Category performance not found in report'); }); it('should correctly identify opportunities (scores < 1)', async () => { vi.spyOn(l1GetReport, 'executeL1GetReport').mockResolvedValue({ reportId: 'test-report', data: mockReport as any, metadata: { url: 'https://example.com', device: 'mobile', categories: ['performance'], timestamp: Date.now(), }, }); const result = await executeL2ScoreAnalysis({ reportId: 'test-report', category: 'performance', }); const opportunities = result.scoreAnalysis.opportunities; // All opportunities should have score < 1 opportunities.forEach(opp => { expect(opp.score).toBeLessThan(1); }); // Check that perfect scores are not in opportunities const clsOpportunity = opportunities.find(o => o.id === 'cumulative-layout-shift'); expect(clsOpportunity).toBeUndefined(); }); it('should assign impact levels based on weight', async () => { vi.spyOn(l1GetReport, 'executeL1GetReport').mockResolvedValue({ reportId: 'test-report', data: mockReport as any, metadata: { url: 'https://example.com', device: 'mobile', categories: ['performance'], timestamp: Date.now(), }, }); const result = await executeL2ScoreAnalysis({ reportId: 'test-report', category: 'performance', }); const tbtOpp = result.scoreAnalysis.opportunities.find(o => o.id === 'total-blocking-time'); expect(tbtOpp?.impact).toBe('high'); // weight 0.3 > 0.1 const fcpOpp = result.scoreAnalysis.opportunities.find(o => o.id === 'first-contentful-paint'); expect(fcpOpp?.impact).toBe('medium'); // weight 0.1 > 0.05 // Add a low weight audit for testing const lowWeightAnalysis = { categories: { performance: { score: 0.85, audits: [ { id: 'low-weight-audit', title: 'Low Weight Audit', score: 0.5, weight: 0.03, weightedScore: 0.015, displayValue: 'test', }, ], }, }, }; vi.spyOn(scores, 'analyzeReport').mockReturnValue(lowWeightAnalysis as any); const result2 = await executeL2ScoreAnalysis({ reportId: 'test-report', category: 'performance', }); const lowOpp = result2.scoreAnalysis.opportunities[0]; expect(lowOpp.impact).toBe('low'); // weight 0.03 < 0.05 }); it('should handle null scores correctly', async () => { const analysisWithNullScores = { categories: { performance: { score: 0.85, audits: [ { id: 'audit-with-score', title: 'Audit With Score', score: 0.8, weight: 0.5, weightedScore: 0.4, displayValue: 'test', }, { id: 'audit-without-score', title: 'Audit Without Score', score: null, weight: 0.5, weightedScore: 0, displayValue: 'N/A', }, ], }, }, }; vi.spyOn(scores, 'analyzeReport').mockReturnValue(analysisWithNullScores as any); vi.spyOn(l1GetReport, 'executeL1GetReport').mockResolvedValue({ reportId: 'test-report', data: mockReport as any, metadata: { url: 'https://example.com', device: 'mobile', categories: ['performance'], timestamp: Date.now(), }, }); const result = await executeL2ScoreAnalysis({ reportId: 'test-report', category: 'performance', }); // Null scores should be filtered out of opportunities expect(result.scoreAnalysis.opportunities).toHaveLength(1); expect(result.scoreAnalysis.opportunities[0].id).toBe('audit-with-score'); // But should still appear in diagnostics with score 0 const nullAudit = result.scoreAnalysis.diagnostics.find(d => d.id === 'audit-without-score'); expect(nullAudit?.score).toBe(0); }); it('should preserve displayValue in all sections', async () => { vi.spyOn(l1GetReport, 'executeL1GetReport').mockResolvedValue({ reportId: 'test-report', data: mockReport as any, metadata: { url: 'https://example.com', device: 'mobile', categories: ['performance'], timestamp: Date.now(), }, }); const result = await executeL2ScoreAnalysis({ reportId: 'test-report', category: 'performance', }); const fcpMetric = result.scoreAnalysis.weightedMetrics.find(m => m.id === 'first-contentful-paint'); expect(fcpMetric?.displayValue).toBe('1.8 s'); const fcpDiagnostic = result.scoreAnalysis.diagnostics.find(d => d.id === 'first-contentful-paint'); expect(fcpDiagnostic?.displayValue).toBe('1.8 s'); }); }); });

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/mizchi/lighthouse-mcp'

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