Spaces:
Sleeping
Sleeping
CarouselForge Developer commited on
Commit ·
e1e95a0
1
Parent(s): 0f4e866
feat: add InsightsBadge component to display carousel guidance
Browse files- jest.config.js +19 -0
- jest.setup.js +2 -0
- src/components/carousel/InsightsBadge.test.tsx +85 -0
- src/components/carousel/InsightsBadge.tsx +45 -0
jest.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const nextJest = require('next/jest')
|
| 2 |
+
|
| 3 |
+
const createJestConfig = nextJest({
|
| 4 |
+
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
| 5 |
+
dir: './',
|
| 6 |
+
})
|
| 7 |
+
|
| 8 |
+
// Add any custom config to be passed to Jest
|
| 9 |
+
const customJestConfig = {
|
| 10 |
+
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
| 11 |
+
testEnvironment: 'jsdom',
|
| 12 |
+
moduleNameMapper: {
|
| 13 |
+
'^@/(.*)$': '<rootDir>/src/$1',
|
| 14 |
+
},
|
| 15 |
+
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
| 19 |
+
module.exports = createJestConfig(customJestConfig)
|
jest.setup.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// jest.setup.js is a place to add global test setup/teardown
|
| 2 |
+
import '@testing-library/jest-dom';
|
src/components/carousel/InsightsBadge.test.tsx
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { render, screen } from '@testing-library/react';
|
| 3 |
+
import InsightsBadge from './InsightsBadge';
|
| 4 |
+
import type { GeneratedInsight } from '@/lib/analytics/insights';
|
| 5 |
+
|
| 6 |
+
describe('InsightsBadge', () => {
|
| 7 |
+
it('should render null when no insights provided', () => {
|
| 8 |
+
const { container } = render(<InsightsBadge insights={[]} />);
|
| 9 |
+
expect(container.firstChild).toBeNull();
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
it('should display insight count when insights provided', () => {
|
| 13 |
+
const insights: GeneratedInsight[] = [
|
| 14 |
+
{
|
| 15 |
+
id: 'test-1',
|
| 16 |
+
observation: 'Bold statement template performed well',
|
| 17 |
+
confidence_level: 'high',
|
| 18 |
+
applies_to: 'template',
|
| 19 |
+
recommendation: 'Use bold-statement',
|
| 20 |
+
created_at: new Date().toISOString(),
|
| 21 |
+
},
|
| 22 |
+
];
|
| 23 |
+
|
| 24 |
+
render(<InsightsBadge insights={insights} />);
|
| 25 |
+
expect(screen.getByText(/1 insight/i)).toBeInTheDocument();
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
it('should show high confidence indicator', () => {
|
| 29 |
+
const insights: GeneratedInsight[] = [
|
| 30 |
+
{
|
| 31 |
+
id: 'test-1',
|
| 32 |
+
observation: 'Test observation',
|
| 33 |
+
confidence_level: 'high',
|
| 34 |
+
applies_to: 'template',
|
| 35 |
+
recommendation: 'Test recommendation',
|
| 36 |
+
created_at: new Date().toISOString(),
|
| 37 |
+
},
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
render(<InsightsBadge insights={insights} />);
|
| 41 |
+
expect(screen.getByText(/Based on your past performance/i)).toBeInTheDocument();
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
it('should display recommendation text in user-friendly language', () => {
|
| 45 |
+
const insights: GeneratedInsight[] = [
|
| 46 |
+
{
|
| 47 |
+
id: 'test-1',
|
| 48 |
+
observation: 'Bold statement template got 45% engagement',
|
| 49 |
+
confidence_level: 'high',
|
| 50 |
+
applies_to: 'template',
|
| 51 |
+
recommendation: 'Use bold-statement for maximum impact',
|
| 52 |
+
created_at: new Date().toISOString(),
|
| 53 |
+
},
|
| 54 |
+
];
|
| 55 |
+
|
| 56 |
+
render(<InsightsBadge insights={insights} />);
|
| 57 |
+
expect(screen.getByText(/Use bold-statement/i)).toBeInTheDocument();
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
it('should display multiple insights in a list', () => {
|
| 61 |
+
const insights: GeneratedInsight[] = [
|
| 62 |
+
{
|
| 63 |
+
id: 'test-1',
|
| 64 |
+
observation: 'Bold statement template performed best',
|
| 65 |
+
confidence_level: 'high',
|
| 66 |
+
applies_to: 'template',
|
| 67 |
+
recommendation: 'Use bold-statement',
|
| 68 |
+
created_at: new Date().toISOString(),
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
id: 'test-2',
|
| 72 |
+
observation: 'Blue palette drove engagement',
|
| 73 |
+
confidence_level: 'medium',
|
| 74 |
+
applies_to: 'palette',
|
| 75 |
+
recommendation: 'Try blue palette',
|
| 76 |
+
created_at: new Date().toISOString(),
|
| 77 |
+
},
|
| 78 |
+
];
|
| 79 |
+
|
| 80 |
+
render(<InsightsBadge insights={insights} />);
|
| 81 |
+
expect(screen.getByText(/2 insights/i)).toBeInTheDocument();
|
| 82 |
+
expect(screen.getByText(/Use bold-statement/i)).toBeInTheDocument();
|
| 83 |
+
expect(screen.getByText(/Try blue palette/i)).toBeInTheDocument();
|
| 84 |
+
});
|
| 85 |
+
});
|
src/components/carousel/InsightsBadge.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import React from 'react';
|
| 4 |
+
import type { GeneratedInsight } from '@/lib/analytics/insights';
|
| 5 |
+
|
| 6 |
+
interface InsightsBadgeProps {
|
| 7 |
+
insights: GeneratedInsight[];
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Display insights that guided carousel creation.
|
| 12 |
+
* Shows in the UI during carousel review before publishing.
|
| 13 |
+
*/
|
| 14 |
+
export default function InsightsBadge({ insights }: InsightsBadgeProps) {
|
| 15 |
+
if (!insights || insights.length === 0) {
|
| 16 |
+
return null;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const count = insights.length;
|
| 20 |
+
const highConfCount = insights.filter((i) => i.confidence_level === 'high').length;
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
| 24 |
+
<div className="flex items-start gap-3">
|
| 25 |
+
<div className="text-xl">💡</div>
|
| 26 |
+
<div className="flex-1">
|
| 27 |
+
<h3 className="font-semibold text-gray-900 mb-2">
|
| 28 |
+
Based on your past performance
|
| 29 |
+
</h3>
|
| 30 |
+
<p className="text-sm text-gray-600 mb-3">
|
| 31 |
+
{count} {count === 1 ? 'insight' : 'insights'} from your recent carousels are guiding this one{highConfCount > 0 ? ' (high confidence)' : ''}:
|
| 32 |
+
</p>
|
| 33 |
+
<ul className="space-y-2 text-sm">
|
| 34 |
+
{insights.map((insight) => (
|
| 35 |
+
<li key={insight.id} className="flex items-start gap-2">
|
| 36 |
+
<span className="text-gray-400 mt-1">•</span>
|
| 37 |
+
<span className="text-gray-700">{insight.recommendation}</span>
|
| 38 |
+
</li>
|
| 39 |
+
))}
|
| 40 |
+
</ul>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
);
|
| 45 |
+
}
|