diff --git a/ui/desktop/src/components/OllamaSetup.test.tsx b/ui/desktop/src/components/OllamaSetup.test.tsx index 291610e4a3b1..945a8beb0831 100644 --- a/ui/desktop/src/components/OllamaSetup.test.tsx +++ b/ui/desktop/src/components/OllamaSetup.test.tsx @@ -81,7 +81,7 @@ describe('OllamaSetup', () => { render(); await waitFor(() => { - fireEvent.click(screen.getByText('Use a different provider')); + fireEvent.click(screen.getByText('Cancel')); }); expect(mockOnCancel).toHaveBeenCalled(); @@ -156,7 +156,7 @@ describe('OllamaSetup', () => { render(); await waitFor(() => { - expect(screen.getByText(/Ollama is running on your system/)).toBeInTheDocument(); + expect(screen.getByText(/Ollama is detected and running/)).toBeInTheDocument(); }); }); @@ -252,7 +252,7 @@ describe('OllamaSetup', () => { pollCallback!({ isRunning: true, host: 'http://127.0.0.1:11434' }); await waitFor(() => { - expect(screen.getByText('✓ Ollama is running on your system')).toBeInTheDocument(); + expect(screen.getByText('Ollama is detected and running')).toBeInTheDocument(); }); }); }); diff --git a/ui/desktop/src/components/OllamaSetup.tsx b/ui/desktop/src/components/OllamaSetup.tsx index 4bdff6b68986..2d104d0f9632 100644 --- a/ui/desktop/src/components/OllamaSetup.tsx +++ b/ui/desktop/src/components/OllamaSetup.tsx @@ -11,6 +11,7 @@ import { } from '../utils/ollamaDetection'; import { initializeSystem } from '../utils/providerUtils'; import { toastService } from '../toasts'; +import { Ollama } from './icons'; interface OllamaSetupProps { onSuccess: () => void; @@ -144,7 +145,9 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { return (
-
+ {/* Header with icon above heading - left aligned like onboarding cards */} +
+

Ollama Setup

Ollama lets you run AI models for free, private and locally on your computer. @@ -153,8 +156,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { {ollamaDetected ? (

-
-

✓ Ollama is running on your system

+
+ Ollama is detected and running
{modelStatus === 'checking' ? ( @@ -163,11 +166,11 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
) : modelStatus === 'not-available' ? (
-
-

+

+

The {getPreferredModel()} model is not installed

-

+

This model is recommended for the best experience with Goose

@@ -182,12 +185,12 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { ) : modelStatus === 'downloading' ? (
-

+

Downloading {getPreferredModel()}...

{downloadProgress && ( <> -

+

{downloadProgress.status}

{downloadProgress.total && downloadProgress.completed && ( @@ -200,7 +203,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { }} />
-

+

{Math.round((downloadProgress.completed / downloadProgress.total) * 100)}%

@@ -221,8 +224,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
) : (
-
-

Ollama is not detected on your system

+
+ Ollama is not detected on your system
{isPolling ? ( @@ -230,8 +233,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
-

Waiting for Ollama to start...

-

+

Waiting for Ollama to start...

+

Once Ollama is installed and running, we'll automatically detect it.

@@ -253,7 +256,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { onClick={onCancel} className="w-full px-6 py-3 bg-transparent text-text-muted rounded-lg hover:bg-background-muted transition-colors" > - Use a different provider + Cancel
); diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 831a23b52397..761c000904be 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -8,6 +8,8 @@ import { initializeSystem } from '../utils/providerUtils'; import { toastService } from '../toasts'; import { OllamaSetup } from './OllamaSetup'; import { checkOllamaStatus } from '../utils/ollamaDetection'; +import { Goose } from './icons/Goose'; +import { OpenRouter, Ollama } from './icons'; interface ProviderGuardProps { children: React.ReactNode; @@ -73,6 +75,9 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { setOpenRouterSetupState(null); setShowFirstTimeSetup(false); setHasProvider(true); + + // Navigate to chat after successful setup + navigate('/', { replace: true }); } else { throw new Error('Provider or model not found after OpenRouter setup'); } @@ -106,8 +111,6 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL; // Always check for Ollama regardless of provider status - const ollamaStatus = await checkOllamaStatus(); - setOllamaDetected(ollamaStatus.isRunning); if (provider && model) { console.log('ProviderGuard - Provider and model found, continuing normally'); @@ -116,6 +119,9 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { console.log('ProviderGuard - No provider/model configured'); setShowFirstTimeSetup(true); } + const ollamaStatus = await checkOllamaStatus(); + setOllamaDetected(ollamaStatus.isRunning); + } catch (error) { // On error, assume no provider and redirect to welcome console.error('Error checking provider configuration:', error); @@ -170,7 +176,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { if (showOllamaSetup) { return ( -
+
@@ -179,6 +185,8 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { onSuccess={() => { setShowOllamaSetup(false); setHasProvider(true); + // Navigate to chat after successful setup + navigate('/', { replace: true }); }} onCancel={() => { setShowOllamaSetup(false); @@ -192,53 +200,124 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { if (showFirstTimeSetup) { return ( -
-
- -

Welcome to Goose!

-

- Let's get you set up with an AI provider to start using Goose. -

- -
- - - + +
{ + setShowFirstTimeSetup(false); + setShowOllamaSetup(true); + }} + className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group" + > +
+
+ +

+ Ollama +

+
+
+ + + +
+
+

+ Run AI models locally on your computer. Completely free and private with no internet required. +

+
+
- -
+
+
+

+ Other providers +

+
+
+ + + +
+
+

+ If you've already signed up for providers like Anthropic, OpenAI etc, you can enter your own keys. +

+
-

- OpenRouter provides instant access to multiple AI models with a simple setup. - {ollamaDetected - ? ' Ollama is also detected on your system for running models locally.' - : ' You can also install Ollama to run free AI models locally on your computer.'} -

+
+
+
); diff --git a/ui/desktop/src/components/icons/Ollama.tsx b/ui/desktop/src/components/icons/Ollama.tsx new file mode 100644 index 000000000000..4e7b57d36a61 --- /dev/null +++ b/ui/desktop/src/components/icons/Ollama.tsx @@ -0,0 +1,45 @@ +export default function Ollama({ className = '' }) { + return ( + + ); +} diff --git a/ui/desktop/src/components/icons/OpenRouter.tsx b/ui/desktop/src/components/icons/OpenRouter.tsx new file mode 100644 index 000000000000..6a7d760c38e7 --- /dev/null +++ b/ui/desktop/src/components/icons/OpenRouter.tsx @@ -0,0 +1,44 @@ +export default function OpenRouter({ className = '' }) { + return ( + + ); +} diff --git a/ui/desktop/src/components/icons/index.tsx b/ui/desktop/src/components/icons/index.tsx index 7e76ab55b938..556f7dc4bc5d 100644 --- a/ui/desktop/src/components/icons/index.tsx +++ b/ui/desktop/src/components/icons/index.tsx @@ -29,6 +29,8 @@ import { Grape } from './Grape'; import Idea from './Idea'; import LinkedIn from './LinkedIn'; import More from './More'; +import OpenRouter from './OpenRouter'; +import Ollama from './Ollama'; import Refresh from './Refresh'; import SensitiveHidden from './SensitiveHidden'; import SensitiveVisible from './SensitiveVisible'; @@ -80,6 +82,8 @@ export { LinkedIn, Microphone, More, + OpenRouter, + Ollama, Refresh, SensitiveHidden, SensitiveVisible, diff --git a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx index 0f1ce36b7b0c..661342782ae9 100644 --- a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx @@ -14,7 +14,7 @@ const GridLayout = memo(function GridLayout({ children }: { children: React.Reac className="grid gap-4 [&_*]:z-20 p-1" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 200px))', - justifyContent: 'start', + justifyContent: 'center', }} > {children} diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx index a0db2348bbf2..37aa7c373aca 100644 --- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx @@ -5,7 +5,6 @@ import ProviderGrid from './ProviderGrid'; import { useConfig } from '../../ConfigContext'; import { ProviderDetails } from '../../../api/types.gen'; import { initializeSystem } from '../../../utils/providerUtils'; -import WelcomeGooseLogo from '../../WelcomeGooseLogo'; import { toastService } from '../../../toasts'; interface ProviderSettingsProps { @@ -97,31 +96,25 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett ); return ( -
+
- {isOnboarding && ( -
-
- +
+ {/* Consistent header pattern with back button */} +
+
+
+

+ {isOnboarding ? 'Other providers' : 'Provider Configuration Settings'} +

+ {isOnboarding && ( +

+ Select an AI model provider to get started with goose. You'll need to use API keys + generated by each provider which will be encrypted and stored locally. You can + change your provider at any time in settings. +

+ )}
- )} -
- {/* Only show back button if not in onboarding mode */} - {!isOnboarding && } -

- {isOnboarding ? 'Configure your providers' : 'Provider Configuration Settings'} -

- {isOnboarding && ( -

- Select an AI model provider to get started with goose. You'll need to use API keys - generated by each provider which will be encrypted and stored locally. You can change - your provider at any time in settings. -

- )}
diff --git a/ui/desktop/src/styles/main.css b/ui/desktop/src/styles/main.css index a329dabbc327..9aded362616a 100644 --- a/ui/desktop/src/styles/main.css +++ b/ui/desktop/src/styles/main.css @@ -741,3 +741,32 @@ p > code.bg-inline-code { [data-state='collapsed'] [data-slot='sidebar-gap'] { will-change: width; } + + + + +/* Subtle back-and-forth shimmer animation for onboarding card */ +@keyframes shimmer { + 0% { + transform: translateX(-100%); + opacity: 0; + } + 20% { + opacity: 0.15; + } + 50% { + transform: translateX(100%); + opacity: 0.05; + } + 80% { + opacity: 0.15; + } + 100% { + transform: translateX(-100%); + opacity: 0; + } +} + +.animate-shimmer { + animation: shimmer 6s ease-in-out infinite; +} diff --git a/ui/desktop/tests/e2e/app.spec.ts b/ui/desktop/tests/e2e/app.spec.ts index 227b703e9075..3b5ce2b91cb6 100644 --- a/ui/desktop/tests/e2e/app.spec.ts +++ b/ui/desktop/tests/e2e/app.spec.ts @@ -37,11 +37,11 @@ test.beforeEach(async ({ }, testInfo) => { if (mainWindow) { // Get a clean test name without the full hierarchy const testName = testInfo.titlePath[testInfo.titlePath.length - 1]; - + // Get provider name if we're in a provider suite const providerSuite = testInfo.titlePath.find(t => t.startsWith('Provider:')); const providerName = providerSuite ? providerSuite.split(': ')[1] : undefined; - + console.log(`Setting overlay for test: "${testName}"${providerName ? ` (Provider: ${providerName})` : ''}`); await showTestName(mainWindow, testName, providerName); } @@ -56,9 +56,9 @@ test.afterEach(async () => { // Helper function to select a provider async function selectProvider(mainWindow: any, provider: Provider) { console.log(`Selecting provider: ${provider.name}`); - + // If we're already in the chat interface, we need to reset providers - const chatTextarea = await mainWindow.waitForSelector('[data-testid="chat-input"]', { + const chatTextarea = await mainWindow.waitForSelector('[data-testid="chat-input"]', { timeout: 2000 }).catch(() => null); @@ -76,7 +76,7 @@ async function selectProvider(mainWindow: any, provider: Provider) { timeout: 5000, state: 'visible' }); - + const modelsTab = await mainWindow.waitForSelector('[data-testid="settings-models-tab"]'); await modelsTab.click(); @@ -90,7 +90,7 @@ async function selectProvider(mainWindow: any, provider: Provider) { state: 'visible' }); await resetButton.click(); - + // Wait for the reset to complete await mainWindow.waitForTimeout(1000); } @@ -117,13 +117,13 @@ async function selectProvider(mainWindow: any, provider: Provider) { } // Check if we need to click "configure other providers (advanced)" button - const configureAdvancedButton = await mainWindow.waitForSelector('button:has-text("configure other providers (advanced)")', { + const configureAdvancedButton = await mainWindow.waitForSelector('h3:has-text("Other providers")', { timeout: 3000, state: 'visible' }).catch(() => null); if (configureAdvancedButton) { - console.log('Found "configure other providers (advanced)" button, clicking it...'); + console.log('Found "configure other providers" button, clicking it...'); await configureAdvancedButton.click(); await mainWindow.waitForTimeout(1500); } @@ -213,7 +213,7 @@ test.describe('Goose App', () => { // Get the main window once for all tests mainWindow = await electronApp.firstWindow(); await mainWindow.waitForLoadState('domcontentloaded'); - + // Try to wait for networkidle, but don't fail if it times out due to MCP activity try { await mainWindow.waitForLoadState('networkidle', { timeout: 10000 }); @@ -307,13 +307,13 @@ test.describe('Goose App', () => { timeout: 5000, state: 'visible' }); - + const appTab = await mainWindow.waitForSelector('[data-testid="settings-app-tab"]'); await appTab.click(); // Wait for the theme selector to be visible await mainWindow.waitForTimeout(1000); - + // Find and click the dark mode toggle button const darkModeButton = await mainWindow.waitForSelector('[data-testid="dark-mode-button"]'); const lightModeButton = await mainWindow.waitForSelector('[data-testid="light-mode-button"]'); @@ -341,13 +341,13 @@ test.describe('Goose App', () => { // check that system mode is clickable await systemModeButton.click(); - + // Toggle back to light mode await lightModeButton.click(); - + // Pause to show return to original state await mainWindow.waitForTimeout(2000); - + // Navigate back to home const homeButton = await mainWindow.waitForSelector('[data-testid="sidebar-home-button"]'); await homeButton.click(); @@ -364,75 +364,75 @@ test.describe('Goose App', () => { test.describe('Chat', () => { test('chat interaction', async () => { console.log(`Testing chat interaction with ${provider.name}...`); - + // Find the chat input const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]'); expect(await chatInput.isVisible()).toBe(true); - + // Type a message await chatInput.fill('Hello, can you help me with a simple task?'); - + // Take screenshot before sending await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-send.png` }); - + // Send message await chatInput.press('Enter'); - + // Wait for loading indicator to appear console.log('Waiting for loading indicator...'); const loadingGoose = await mainWindow.waitForSelector('[data-testid="loading-indicator"]', { timeout: 2000 }); expect(await loadingGoose.isVisible()).toBe(true); - + // Take screenshot of loading state await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-loading-state.png` }); - + // Wait for loading indicator to disappear console.log('Waiting for response...'); await mainWindow.waitForSelector('[data-testid="loading-indicator"]', { state: 'hidden', timeout: 30000 }); - + // Get the latest response const response = await mainWindow.locator('[data-testid="message-container"]').last(); expect(await response.isVisible()).toBe(true); - + // Verify response has content const responseText = await response.textContent(); expect(responseText).toBeTruthy(); expect(responseText.length).toBeGreaterThan(0); - + // Take screenshot of response await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-chat-response.png` }); }); - + test('verify chat history', async () => { console.log(`Testing chat history with ${provider.name}...`); - + // Find the chat input again const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]'); - + // Test message sending with a specific question await chatInput.fill('What is 2+2?'); - + // Send message await chatInput.press('Enter'); - + // Wait for loading indicator and response await mainWindow.waitForSelector('[data-testid="loading-indicator"]', { state: 'hidden', timeout: 30000 }); - + // Get the latest response const response = await mainWindow.locator('[data-testid="message-container"]').last(); const responseText = await response.textContent(); expect(responseText).toBeTruthy(); - + // Check for message history const messages = await mainWindow.locator('[data-testid="message-container"]').all(); expect(messages.length).toBeGreaterThanOrEqual(2); - + // Take screenshot of chat history await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-chat-history.png` }); - + // Test command history (up arrow) await chatInput.press('Control+ArrowUp'); const inputValue = await chatInput.inputValue(); @@ -443,7 +443,7 @@ test.describe('Goose App', () => { test.describe('MCP Integration', () => { test('running quotes MCP server integration', async () => { console.log(`Testing Running Quotes MCP server integration with ${provider.name}...`); - + // Create test-results directory if it doesn't exist const fs = require('fs'); if (!fs.existsSync('test-results')) { @@ -460,7 +460,7 @@ test.describe('Goose App', () => { console.log('NetworkIdle timeout (likely due to MCP activity), continuing with test...'); } await mainWindow.waitForLoadState('domcontentloaded'); - + // Wait for React app to be ready await mainWindow.waitForFunction(() => { const root = document.getElementById('root'); @@ -477,47 +477,47 @@ test.describe('Goose App', () => { state: 'visible' }); await extensionsButton.click(); - + // Wait for extensions page to load await mainWindow.waitForTimeout(1000); - + // Look for Running Quotes extension card console.log('Looking for existing Running Quotes extension...'); const existingExtension = await mainWindow.$('div.flex:has-text("Running Quotes")'); - + if (existingExtension) { console.log('Found existing Running Quotes extension, removing it...'); - + // Find and click the settings gear icon next to Running Quotes const settingsButton = await existingExtension.$('button[aria-label="Extension settings"]'); if (settingsButton) { await settingsButton.click(); - + // Wait for modal to appear await mainWindow.waitForTimeout(500); - + // Click the Remove Extension button const removeButton = await mainWindow.waitForSelector('button:has-text("Remove Extension")', { timeout: 2000, state: 'visible' }); await removeButton.click(); - + // Wait for confirmation modal await mainWindow.waitForTimeout(500); - + // Click the Remove button in confirmation dialog const confirmButton = await mainWindow.waitForSelector('button:has-text("Remove")', { timeout: 2000, state: 'visible' }); await confirmButton.click(); - + // Wait for extension to be removed await mainWindow.waitForTimeout(1000); } } - + // Now proceed with adding the extension console.log('Proceeding with adding Running Quotes extension...'); @@ -527,11 +527,11 @@ test.describe('Goose App', () => { timeout: 2000, state: 'visible' }); - + // Verify add extension button is visible const isAddExtensionVisible = await addExtensionButton.isVisible(); console.log('Add custom extension button visible:', isAddExtensionVisible); - + await addExtensionButton.click(); console.log('Clicked Add custom extension'); @@ -541,21 +541,21 @@ test.describe('Goose App', () => { // Fill the form console.log('Filling form fields...'); - + // Fill Extension Name const nameInput = await mainWindow.waitForSelector('input[placeholder="Enter extension name..."]', { timeout: 2000, state: 'visible' }); await nameInput.fill('Running Quotes'); - + // Fill Description const descriptionInput = await mainWindow.waitForSelector('input[placeholder="Optional description..."]', { timeout: 2000, state: 'visible' }); await descriptionInput.fill('Inspirational running quotes MCP server'); - + // Fill Command const mcpScriptPath = join(__dirname, 'basic-mcp.ts'); const commandInput = await mainWindow.waitForSelector('input[placeholder="e.g. npx -y @modelcontextprotocol/my-extension "]', { @@ -576,50 +576,50 @@ test.describe('Goose App', () => { timeout: 2000, state: 'visible' }); - + // Verify button is visible const isModalAddButtonVisible = await modalAddButton.isVisible(); console.log('Add Extension button visible:', isModalAddButtonVisible); // Click the button await modalAddButton.click(); - + console.log('Clicked Add Extension button'); // Wait for the Running Quotes extension to appear in the list console.log('Waiting for Running Quotes extension to appear...'); try { const extensionCard = await mainWindow.waitForSelector( - 'div.flex:has-text("Running Quotes")', + 'div.flex:has-text("Running Quotes")', { timeout: 30000, state: 'visible' } ); - + // Verify the extension is enabled await mainWindow.waitForTimeout(1000); const toggleButton = await extensionCard.$('button[role="switch"][data-state="checked"]'); const isEnabled = !!toggleButton; console.log('Extension enabled:', isEnabled); - + if (!isEnabled) { throw new Error('Running Quotes extension was added but not enabled'); } - + await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-extension-added.png` }); console.log('Extension added successfully'); } catch (error) { console.error('Error verifying extension:', error); - + // Get any error messages that might be visible - const errorElements = await mainWindow.$$eval('.text-red-500, .text-error', + const errorElements = await mainWindow.$$eval('.text-red-500, .text-error', elements => elements.map(el => el.textContent) ); if (errorElements.length > 0) { console.log('Found error messages:', errorElements); } - + throw error; } @@ -631,36 +631,36 @@ test.describe('Goose App', () => { } catch (error) { // Take error screenshot and log details await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-error.png` }); - + // Get page content const pageContent = await mainWindow.evaluate(() => document.body.innerHTML); console.log('Page content at error:', pageContent); - + console.error('Test failed:', error); throw error; } }); - + test('test running quotes functionality', async () => { console.log(`Testing running quotes functionality with ${provider.name}...`); - + // Find the chat input const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]'); expect(await chatInput.isVisible()).toBe(true); - + // Type a message requesting a running quote await chatInput.fill('Can you give me an inspirational running quote using the runningQuotes tool?'); - + // Take screenshot before sending await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-quote-request.png` }); - + // Send message await chatInput.press('Enter'); // Get the latest response const response = await mainWindow.waitForSelector('.goose-message-tool', { timeout: 5000 }); expect(await response.isVisible()).toBe(true); - + // Click the Output dropdown to reveal the actual quote await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response-debug.png` }); @@ -668,12 +668,12 @@ test.describe('Goose App', () => { const outputContent = await mainWindow.waitForSelector('.whitespace-pre-wrap', { timeout: 5000 }); const outputText = await outputContent.textContent(); console.log('Output text:', outputText); - + // Take screenshot of expanded response await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response.png` }); - + // Check if the output contains one of our known quotes - const containsKnownQuote = runningQuotes.some(({ quote, author }) => + const containsKnownQuote = runningQuotes.some(({ quote, author }) => outputText.includes(`"${quote}" - ${author}`) ); expect(containsKnownQuote).toBe(true);