{ + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MOCK_COMPLETION_RESPONSE_1) + }); + }); + + await sendButton.click(); + await expect(page.locator('article', { hasText: 'I am unable to provide subjective or speculative information, including fight outcomes between individuals.' })).toBeVisible(); + + await promptTextarea.fill("I understand, it's okay"); + await expect(page.locator('article', { hasText: 'No problem! If you have any other questions or would like to discuss something else, feel free to ask' })).not.toBeVisible(); + await expect(page.locator('article', { hasText: "I understand, it's okay" })).not.toBeVisible(); + + await page.route('**/generate', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MOCK_COMPLETION_RESPONSE_2) + }); + }); + + await page.keyboard.press('Enter'); + await expect(page.locator('article', { hasText: "I understand, it's okay" })).toBeVisible(); + await expect(page.locator('article', { hasText: 'No problem! If you have any other questions or would like to discuss something else, feel free to ask' })).toBeVisible(); + await expect(page.getByText('AI')).toHaveCount(2); + expect(await page.getByText('You').count()).toBeGreaterThan(1); + + // Check the session is saved to localStorage + await newSessionButton.click(); + await expect(articleLocator).toHaveCount(0); + + await page.goBack(); + await expect(articleLocator).toHaveCount(4); +}); + +test('generates a random session id', async ({ page }) => { + await page.goto('/'); + + const sessionIds = []; + const newSessionButton = page.getByText('New session'); + + // Check it generates a new session id 3 times in a row + for (let i = 0; i < 3; i++) { + const sessionId = await newSessionButton.getAttribute('href'); + expect(sessionId).toMatch(/[a-z0-9]{2,8}/); + sessionIds.push(sessionId); + await newSessionButton.click(); + } + + expect(new Set(sessionIds).size).toBe(3); +}); + +test.skip('handles API error when generating AI response', async ({ page }) => { + // TODO: Implement the test +}); + +test.skip('displays system message when an error occurs', async ({ page }) => { + // TODO: Implement the test +}); + +test.skip('auto-scrolls to the bottom when new messages are added', async ({ page }) => { + // TODO: Implement the test +}); diff --git a/tests/settings.test.ts b/tests/settings.test.ts new file mode 100644 index 00000000..52c67cee --- /dev/null +++ b/tests/settings.test.ts @@ -0,0 +1,75 @@ +import { expect, test } from '@playwright/test'; +import type { OllamaTagResponse } from '$lib/ollama'; +import { MOCK_API_TAGS_RESPONSE } from './utils'; + +test.beforeEach(async ({ page }) => { + // Enable request interception + await page.route('**/api/tags', async (route) => { + // Fulfill the request with the mock response + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MOCK_API_TAGS_RESPONSE) + }); + }); +}); + +test('displays model list and updates settings store', async ({ page }) => { + await page.goto('/'); + + // Wait for the model list to be loaded + await page.waitForSelector('label:has-text("Model")'); + + // Click on the button that opens the model list + await page.click('button[data-melt-select-trigger]'); + + // Wait for the model list to be visible + await page.waitForSelector('div[role="listbox"]'); + + // Check if the model list contains the expected models + const modelNames = await page.$$eval('div[role="option"]', options => + options.map(option => option.textContent?.trim()) + ); + expect(modelNames).toContain('gemma:7b'); + expect(modelNames).toContain('mistral:latest'); + + // Select a model by clicking on its name + await page.click('div[role="option"]:has-text("mistral:latest")'); + + // Check if the settings store is updated with the selected model + const localStorageValue = await page.evaluate(() => window.localStorage.getItem('hollama-settings')); + expect(localStorageValue).toContain('"ollamaModel":"mistral:latest"'); +}); + +test('handles server status updates correctly', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('label:has-text("Server")'); + + // Mock the API to return a successful response + await page.route('**/api/tags', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MOCK_API_TAGS_RESPONSE) + }); + }); + + // Trigger a new API request by typing in the input field + await page.fill('input[placeholder="http://localhost:11434"]', 'http://example.com'); + + // Wait for the server status to be updated to "connected" + await expect(page.getByText('connected', { exact: true })).toBeVisible(); + await expect(page.getByText('connected', { exact: true })).toHaveClass(/bg-emerald-600/); + + // Mock the API to return an error response + await page.route('**/api/tags', async (route) => { + await route.abort(); + }); + + // Trigger a new API request by typing in the input field + await page.fill('input[placeholder="http://localhost:11434"]', 'http://example.com/invalid'); + + // Wait for the server status to be updated to "disconnected" + await expect(page.getByText('disconnected')).toBeVisible(); + await expect(page.getByText('disconnected')).toHaveClass(/bg-amber-600/); +}); diff --git a/tests/test.ts b/tests/test.ts deleted file mode 100644 index 5816be41..00000000 --- a/tests/test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('index page has expected h1', async ({ page }) => { - await page.goto('/'); - await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible(); -}); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 00000000..423618dd --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,183 @@ +import type { OllamaCompletionResponse, OllamaTagResponse } from "$lib/ollama"; + +export const MOCK_API_TAGS_RESPONSE: OllamaTagResponse = { + models: [ + { + name: 'gemma:7b', + model: 'gemma:7b', + modified_at: '2024-04-08T21:41:35.217983842-04:00"', + size: 5011853225, + digest: 'a72c7f4d0a15522df81486d13ce432c79e191bda2558d024fbad4362c4f7cbf8', + details: { + parent_model: '', + format: 'gguf', + family: 'gemma', + families: ["gemma"], + parameter_size: '9B', + quantization_level: 'Q4_0' + } + }, + { + name: "mistral:latest", + model: "mistral:latest", + modified_at: "2023-11-24T16:32:44.035655802-05:00", + size: 4108916866, + digest: "d364aa8d131ef7abfc1275db682d281a307d9451fc00f96abe154d0059b0be49", + details: { + parent_model: "", + format: "gguf", + family: "llama", + families: null, + parameter_size: "7B", + quantization_level: "Q4_0" + } + } + ] +}; + +export const MOCK_COMPLETION_RESPONSE_1: OllamaCompletionResponse = { + model: "gemma:7b", + created_at: "2024-04-10T22:54:40.310905Z", + response: "I am unable to provide subjective or speculative information, including fight outcomes between individuals.", + done: true, + context: [ + 106, + 1645, + 108, + 6571, + 1134, + 3709, + 575, + 476, + 5900, + 1865, + 29349, + 29678, + 578, + 36171, + 68187, + 235336, + 107, + 108, + 106, + 2516, + 108, + 235285, + 1144, + 14321, + 577, + 3658, + 45927, + 689, + 85979, + 2113, + 235269, + 3359, + 5900, + 18701, + 1865, + 9278, + 235265, + 107, + 108 + ], + total_duration: 564546083, + load_duration: 419166, + prompt_eval_count: 18, + prompt_eval_duration: 267210000, + eval_count: 17, + eval_duration: 296374000, +}; + +export const MOCK_COMPLETION_RESPONSE_2: OllamaCompletionResponse = { + model: "gemma:7b", + created_at: "2024-04-10T23:08:33.419483Z", + response: "No problem! If you have any other questions or would like to discuss something else, feel free to ask.", + done: true, + context: [ + 106, + 1645, + 108, + 6571, + 1134, + 3709, + 575, + 476, + 5900, + 1865, + 29349, + 29678, + 578, + 36171, + 68187, + 235336, + 107, + 108, + 106, + 2516, + 108, + 235285, + 1144, + 14321, + 577, + 3658, + 45927, + 689, + 85979, + 2113, + 235269, + 3359, + 5900, + 18701, + 1865, + 9278, + 235265, + 107, + 108, + 106, + 1645, + 108, + 235285, + 3508, + 235269, + 665, + 235303, + 235256, + 12763, + 107, + 108, + 106, + 2516, + 108, + 1294, + 3210, + 235341, + 1927, + 692, + 791, + 1089, + 1156, + 3920, + 689, + 1134, + 1154, + 577, + 9742, + 2775, + 1354, + 235269, + 2375, + 2223, + 577, + 5291, + 235265, + 107, + 108 + ], + total_duration: 1574338000, + load_duration: 1044484792, + prompt_eval_count: 55, + prompt_eval_duration: 130165000, + eval_count: 23, + eval_duration: 399362000, +};