Skip to content

Commit d24f947

Browse files
authored
fix(ui): Add tracing inline settings back and create UI tests (mudler#9027)
Signed-off-by: localai-bot <localai-bot@localai.io>
1 parent b9cba9a commit d24f947

25 files changed

Lines changed: 789 additions & 235 deletions

.github/workflows/tests-ui-e2e.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
name: 'UI E2E Tests'
3+
4+
on:
5+
pull_request:
6+
paths:
7+
- 'core/http/**'
8+
- 'tests/e2e-ui/**'
9+
- 'tests/e2e/mock-backend/**'
10+
push:
11+
branches:
12+
- master
13+
14+
concurrency:
15+
group: ci-tests-ui-e2e-${{ github.head_ref || github.ref }}-${{ github.repository }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
tests-ui-e2e:
20+
runs-on: ubuntu-latest
21+
strategy:
22+
matrix:
23+
go-version: ['1.26.x']
24+
steps:
25+
- name: Clone
26+
uses: actions/checkout@v6
27+
with:
28+
submodules: true
29+
- name: Setup Go ${{ matrix.go-version }}
30+
uses: actions/setup-go@v5
31+
with:
32+
go-version: ${{ matrix.go-version }}
33+
cache: false
34+
- name: Setup Node.js
35+
uses: actions/setup-node@v6
36+
with:
37+
node-version: '22'
38+
- name: Proto Dependencies
39+
run: |
40+
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-linux-x86_64.zip -o protoc.zip && \
41+
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
42+
rm protoc.zip
43+
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
44+
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
45+
- name: System Dependencies
46+
run: |
47+
sudo apt-get update
48+
sudo apt-get install -y build-essential libopus-dev
49+
- name: Build UI test server
50+
run: PATH="$PATH:$HOME/go/bin" make build-ui-test-server
51+
- name: Install Playwright
52+
working-directory: core/http/react-ui
53+
run: |
54+
npm install
55+
npx playwright install --with-deps chromium
56+
- name: Run Playwright tests
57+
working-directory: core/http/react-ui
58+
run: npx playwright test
59+
- name: Upload Playwright report
60+
if: ${{ failure() }}
61+
uses: actions/upload-artifact@v4
62+
with:
63+
name: playwright-report
64+
path: core/http/react-ui/playwright-report/
65+
retention-days: 7
66+
- name: Setup tmate session if tests fail
67+
if: ${{ failure() }}
68+
uses: mxschmitt/action-tmate@v3.23
69+
with:
70+
detached: true
71+
connect-timeout-seconds: 180
72+
limit-access-to-actor: true

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,8 @@ core/http/react-ui/dist
7272

7373
# Extracted backend binaries for container-based testing
7474
local-backends/
75+
76+
# UI E2E test artifacts
77+
tests/e2e-ui/ui-test-server
78+
core/http/react-ui/playwright-report/
79+
core/http/react-ui/test-results/

Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,23 @@ build-mock-backend: protogen-go
646646
clean-mock-backend:
647647
rm -f tests/e2e/mock-backend/mock-backend
648648

649+
########################################################
650+
### UI E2E Test Server
651+
########################################################
652+
653+
build-ui-test-server: build-mock-backend react-ui protogen-go
654+
$(GOCMD) build -o tests/e2e-ui/ui-test-server ./tests/e2e-ui
655+
656+
test-ui-e2e: build-ui-test-server
657+
cd core/http/react-ui && npm install && npx playwright install --with-deps chromium && npx playwright test
658+
659+
test-ui-e2e-docker:
660+
docker build -t localai-ui-e2e -f tests/e2e-ui/Dockerfile .
661+
docker run --rm localai-ui-e2e
662+
663+
clean-ui-test-server:
664+
rm -f tests/e2e-ui/ui-test-server
665+
649666
########################################################
650667
### END Backends
651668
########################################################
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test.describe('Navigation', () => {
4+
test('/ redirects to /app', async ({ page }) => {
5+
await page.goto('/')
6+
await expect(page).toHaveURL(/\/app/)
7+
})
8+
9+
test('/app shows home page with LocalAI title', async ({ page }) => {
10+
await page.goto('/app')
11+
await expect(page.locator('.sidebar')).toBeVisible()
12+
await expect(page.getByRole('heading', { name: 'How can I help you today?' })).toBeVisible()
13+
})
14+
15+
test('sidebar traces link navigates to /app/traces', async ({ page }) => {
16+
await page.goto('/app')
17+
const tracesLink = page.locator('a.nav-item[href="/app/traces"]')
18+
await expect(tracesLink).toBeVisible()
19+
await tracesLink.click()
20+
await expect(page).toHaveURL(/\/app\/traces/)
21+
await expect(page.getByRole('heading', { name: 'Traces', exact: true })).toBeVisible()
22+
})
23+
})
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test.describe('Traces Settings', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/app/traces')
6+
// Wait for settings panel to load
7+
await expect(page.locator('text=Tracing is')).toBeVisible({ timeout: 10_000 })
8+
})
9+
10+
test('settings panel is visible on page load', async ({ page }) => {
11+
await expect(page.locator('text=Tracing is')).toBeVisible()
12+
})
13+
14+
test('expand and collapse settings', async ({ page }) => {
15+
// The test server starts with tracing enabled, so the panel starts collapsed
16+
const settingsHeader = page.locator('button', { hasText: 'Tracing is' })
17+
18+
// Click to expand
19+
await settingsHeader.click()
20+
await expect(page.locator('text=Enable Tracing')).toBeVisible()
21+
22+
// Click to collapse
23+
await settingsHeader.click()
24+
await expect(page.locator('text=Enable Tracing')).not.toBeVisible()
25+
})
26+
27+
test('toggle tracing on and off', async ({ page }) => {
28+
// Expand settings
29+
const settingsHeader = page.locator('button', { hasText: 'Tracing is' })
30+
await settingsHeader.click()
31+
await expect(page.locator('text=Enable Tracing')).toBeVisible()
32+
33+
// The Toggle component is a <label> wrapping a hidden checkbox.
34+
// Target the checkbox within the settings panel.
35+
const checkbox = page.locator('input[type="checkbox"]')
36+
37+
// Initially enabled (server starts with tracing on)
38+
await expect(checkbox).toBeChecked()
39+
40+
// Click the label (parent) to toggle off
41+
await checkbox.locator('..').click()
42+
await expect(checkbox).not.toBeChecked()
43+
44+
// Click again to re-enable
45+
await checkbox.locator('..').click()
46+
await expect(checkbox).toBeChecked()
47+
})
48+
49+
test('set max items value', async ({ page }) => {
50+
// Expand settings
51+
await page.locator('button', { hasText: 'Tracing is' }).click()
52+
await expect(page.locator('text=Enable Tracing')).toBeVisible()
53+
54+
const maxItemsInput = page.locator('input[type="number"]')
55+
await maxItemsInput.fill('500')
56+
await expect(maxItemsInput).toHaveValue('500')
57+
})
58+
59+
test('save shows toast', async ({ page }) => {
60+
// Expand settings
61+
await page.locator('button', { hasText: 'Tracing is' }).click()
62+
63+
// Click save
64+
await page.locator('button', { hasText: 'Save' }).click()
65+
66+
// Verify toast appears
67+
await expect(page.locator('text=Tracing settings saved')).toBeVisible({ timeout: 5_000 })
68+
})
69+
70+
test('panel collapses after save when tracing is enabled', async ({ page }) => {
71+
// Expand settings
72+
await page.locator('button', { hasText: 'Tracing is' }).click()
73+
await expect(page.locator('text=Enable Tracing')).toBeVisible()
74+
75+
// Tracing is already enabled; save
76+
await page.locator('button', { hasText: 'Save' }).click()
77+
78+
// Panel should collapse
79+
await expect(page.locator('text=Enable Tracing')).not.toBeVisible()
80+
})
81+
82+
test('panel stays expanded after save when tracing is off', async ({ page }) => {
83+
// Expand settings
84+
await page.locator('button', { hasText: 'Tracing is' }).click()
85+
await expect(page.locator('text=Enable Tracing')).toBeVisible()
86+
87+
// Toggle tracing off
88+
await page.locator('input[type="checkbox"]').locator('..').click()
89+
90+
// Save
91+
await page.locator('button', { hasText: 'Save' }).click()
92+
93+
// Panel should stay expanded since tracing is now disabled
94+
await expect(page.locator('text=Enable Tracing')).toBeVisible()
95+
})
96+
})

core/http/react-ui/package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/http/react-ui/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"dev": "vite",
88
"build": "vite build",
99
"preview": "vite preview",
10-
"lint": "eslint ."
10+
"lint": "eslint .",
11+
"test:e2e": "playwright test",
12+
"test:e2e:ui": "playwright test --ui"
1113
},
1214
"dependencies": {
1315
"react": "^19.1.0",
@@ -27,6 +29,7 @@
2729
"@eslint/js": "^9.27.0",
2830
"globals": "^16.1.0",
2931
"eslint-plugin-react-hooks": "^5.2.0",
30-
"eslint-plugin-react-refresh": "^0.4.20"
32+
"eslint-plugin-react-refresh": "^0.4.20",
33+
"@playwright/test": "1.52.0"
3134
}
3235
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { defineConfig } from '@playwright/test'
2+
3+
export default defineConfig({
4+
testDir: './e2e',
5+
timeout: 30_000,
6+
retries: process.env.CI ? 2 : 0,
7+
reporter: process.env.CI ? 'html' : 'list',
8+
use: {
9+
baseURL: 'http://127.0.0.1:8089',
10+
trace: 'on-first-retry',
11+
},
12+
projects: [
13+
{
14+
name: 'chromium',
15+
use: { browserName: 'chromium' },
16+
},
17+
],
18+
webServer: process.env.PLAYWRIGHT_EXTERNAL_SERVER ? undefined : {
19+
command: '../../../tests/e2e-ui/ui-test-server --mock-backend=../../../tests/e2e/mock-backend/mock-backend --port=8089',
20+
port: 8089,
21+
timeout: 120_000,
22+
reuseExistingServer: !process.env.CI,
23+
},
24+
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export default function Modal({ onClose, children, maxWidth = '600px' }) {
2+
return (
3+
<div style={{
4+
position: 'fixed', inset: 0, zIndex: 1000,
5+
display: 'flex', alignItems: 'center', justifyContent: 'center',
6+
background: 'var(--color-modal-backdrop)', backdropFilter: 'blur(4px)',
7+
}} onClick={onClose}>
8+
<div style={{
9+
background: 'var(--color-bg-secondary)',
10+
border: '1px solid var(--color-border-subtle)',
11+
borderRadius: 'var(--radius-lg)',
12+
maxWidth, width: '90%', maxHeight: '80vh',
13+
display: 'flex', flexDirection: 'column',
14+
overflow: 'auto',
15+
}} onClick={e => e.stopPropagation()}>
16+
{children}
17+
</div>
18+
</div>
19+
)
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function SettingRow({ label, description, children }) {
2+
return (
3+
<div style={{
4+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
5+
padding: 'var(--spacing-sm) 0',
6+
borderBottom: '1px solid var(--color-border-subtle)',
7+
}}>
8+
<div style={{ flex: 1, marginRight: 'var(--spacing-md)' }}>
9+
<div style={{ fontSize: '0.875rem', fontWeight: 500 }}>{label}</div>
10+
{description && <div style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', marginTop: 2 }}>{description}</div>}
11+
</div>
12+
<div style={{ flexShrink: 0 }}>{children}</div>
13+
</div>
14+
)
15+
}

0 commit comments

Comments
 (0)