diff --git a/README.md b/README.md index 45497e3..2bdc70f 100644 --- a/README.md +++ b/README.md @@ -376,6 +376,9 @@ pytest tests/ -v # Test con coverage pytest tests/ --cov=app +# Browser E2E test (cache-first navigation) +OLLAMA_HOST=http://192.168.254.115:11434 npm run test:e2e + # Hot reload durante sviluppo uvicorn main:app --reload ``` diff --git a/package.json b/package.json index 1db5236..963585e 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "name": "llm-monitor", "version": "1.0.0", + "type": "commonjs", "description": "Dashboard per controllare i modelli caricati in Ollama", "private": true, "scripts": { "tailwind:dev": "tailwindcss -i ./app/web/static/css/input.css -o ./app/web/static/css/output.css --watch", - "tailwind:build": "tailwindcss -i ./app/web/static/css/input.css -o ./app/web/static/css/output.css" + "tailwind:build": "tailwindcss -i ./app/web/static/css/input.css -o ./app/web/static/css/output.css", + "test:e2e": "playwright test tests/e2e/cache-navigation.spec.js" }, "devDependencies": { + "@playwright/test": "^1.59.1", "tailwindcss": "^3.4.0" } } diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..3ea575d --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,22 @@ +const { defineConfig } = require('@playwright/test'); + +const baseURL = process.env.TARGET_URL || 'http://127.0.0.1:8011'; + +module.exports = defineConfig({ + testDir: './tests/e2e', + timeout: 45000, + fullyParallel: false, + retries: 0, + reporter: 'list', + use: { + baseURL, + headless: true, + serviceWorkers: 'block' + }, + webServer: { + command: 'python3 -m uvicorn main:app --host 127.0.0.1 --port 8011', + url: baseURL, + reuseExistingServer: true, + timeout: 30000 + } +}); \ No newline at end of file diff --git a/tests/e2e/cache-navigation.spec.js b/tests/e2e/cache-navigation.spec.js new file mode 100644 index 0000000..d87afb5 --- /dev/null +++ b/tests/e2e/cache-navigation.spec.js @@ -0,0 +1,78 @@ +const { test, expect } = require('@playwright/test'); + +const OLLAMA_HOST = process.env.OLLAMA_HOST || 'http://192.168.254.115:11434'; +const SERVER_ID = process.env.TEST_SERVER_ID || 'srv_e2e_cache'; +const SERVER_NAME = process.env.TEST_SERVER_NAME || 'E2E Cache Server'; +const QUIET_WINDOW_MS = Number(process.env.QUIET_WINDOW_MS || 1500); +const CACHE_WAIT_TIMEOUT_MS = Number(process.env.CACHE_WAIT_TIMEOUT_MS || 20000); + +test.describe('cache-first server navigation', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript( + ({ serverId, serverName, host }) => { + localStorage.setItem( + 'llm_monitor_servers', + JSON.stringify([ + { + id: serverId, + name: serverName, + host + } + ]) + ); + localStorage.setItem('llm_monitor_active_server', serverId); + }, + { + serverId: SERVER_ID, + serverName: SERVER_NAME, + host: OLLAMA_HOST + } + ); + }); + + test('serves cached data when navigating between running and available pages', async ({ context, page }) => { + const apiRequests = []; + + context.on('request', (request) => { + const url = new URL(request.url()); + if (url.pathname.startsWith('/api/v1/')) { + apiRequests.push(url.pathname + url.search); + } + }); + + const resetApiRequests = () => { + apiRequests.length = 0; + }; + + const waitForQuietWindow = async (label) => { + await page.waitForTimeout(QUIET_WINDOW_MS); + expect(apiRequests, `${label} should not issue API requests while cache is fresh`).toEqual([]); + }; + + await page.goto(`/models-running?server=${SERVER_ID}`, { waitUntil: 'domcontentloaded' }); + + await page.waitForFunction( + (serverId) => { + return ['health', 'models', 'running'].every((suffix) => { + return Boolean(localStorage.getItem(`llm_monitor_${suffix}_${serverId}`)); + }); + }, + SERVER_ID, + { timeout: CACHE_WAIT_TIMEOUT_MS } + ); + + expect(apiRequests.length).toBeGreaterThan(0); + + resetApiRequests(); + await page.goto(`/models-available?server=${SERVER_ID}`, { waitUntil: 'domcontentloaded' }); + await waitForQuietWindow('running -> available'); + + resetApiRequests(); + await page.goto(`/models-running?server=${SERVER_ID}`, { waitUntil: 'domcontentloaded' }); + await waitForQuietWindow('available -> running'); + + resetApiRequests(); + await page.goto(`/models-available?server=${SERVER_ID}`, { waitUntil: 'domcontentloaded' }); + await waitForQuietWindow('running -> available again'); + }); +});