Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/tests-ui-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: 'UI E2E Tests'

on:
pull_request:
paths:
- 'core/http/**'
- 'tests/e2e-ui/**'
- 'tests/e2e/mock-backend/**'
push:
branches:
- master

concurrency:
group: ci-tests-ui-e2e-${{ github.head_ref || github.ref }}-${{ github.repository }}
cancel-in-progress: true

jobs:
tests-ui-e2e:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.26.x']
steps:
- name: Clone
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Proto Dependencies
run: |
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-linux-x86_64.zip -o protoc.zip && \
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
rm protoc.zip
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
- name: System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libopus-dev
- name: Build UI test server
run: PATH="$PATH:$HOME/go/bin" make build-ui-test-server
- name: Install Playwright
working-directory: core/http/react-ui
run: |
npm install
npx playwright install --with-deps chromium
- name: Run Playwright tests
working-directory: core/http/react-ui
run: npx playwright test
- name: Upload Playwright report
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: core/http/react-ui/playwright-report/
retention-days: 7
- name: Setup tmate session if tests fail
if: ${{ failure() }}
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
connect-timeout-seconds: 180
limit-access-to-actor: true
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,8 @@ core/http/react-ui/dist

# Extracted backend binaries for container-based testing
local-backends/

# UI E2E test artifacts
tests/e2e-ui/ui-test-server
core/http/react-ui/playwright-report/
core/http/react-ui/test-results/
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,23 @@ build-mock-backend: protogen-go
clean-mock-backend:
rm -f tests/e2e/mock-backend/mock-backend

########################################################
### UI E2E Test Server

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

########################################################

build-ui-test-server: build-mock-backend react-ui protogen-go
$(GOCMD) build -o tests/e2e-ui/ui-test-server ./tests/e2e-ui

test-ui-e2e: build-ui-test-server
cd core/http/react-ui && npm install && npx playwright install --with-deps chromium && npx playwright test

test-ui-e2e-docker:
docker build -t localai-ui-e2e -f tests/e2e-ui/Dockerfile .
docker run --rm localai-ui-e2e

clean-ui-test-server:
rm -f tests/e2e-ui/ui-test-server

########################################################
### END Backends
########################################################
Expand Down
23 changes: 23 additions & 0 deletions core/http/react-ui/e2e/navigation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test, expect } from '@playwright/test'

test.describe('Navigation', () => {
test('/ redirects to /app', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveURL(/\/app/)
})

test('/app shows home page with LocalAI title', async ({ page }) => {
await page.goto('/app')
await expect(page.locator('.sidebar')).toBeVisible()
await expect(page.getByRole('heading', { name: 'How can I help you today?' })).toBeVisible()
})

test('sidebar traces link navigates to /app/traces', async ({ page }) => {
await page.goto('/app')
const tracesLink = page.locator('a.nav-item[href="/app/traces"]')
await expect(tracesLink).toBeVisible()
await tracesLink.click()
await expect(page).toHaveURL(/\/app\/traces/)
await expect(page.getByRole('heading', { name: 'Traces', exact: true })).toBeVisible()
})
})
96 changes: 96 additions & 0 deletions core/http/react-ui/e2e/traces.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { test, expect } from '@playwright/test'

test.describe('Traces Settings', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/app/traces')
// Wait for settings panel to load
await expect(page.locator('text=Tracing is')).toBeVisible({ timeout: 10_000 })
})

test('settings panel is visible on page load', async ({ page }) => {
await expect(page.locator('text=Tracing is')).toBeVisible()
})

test('expand and collapse settings', async ({ page }) => {
// The test server starts with tracing enabled, so the panel starts collapsed
const settingsHeader = page.locator('button', { hasText: 'Tracing is' })

// Click to expand
await settingsHeader.click()
await expect(page.locator('text=Enable Tracing')).toBeVisible()

// Click to collapse
await settingsHeader.click()
await expect(page.locator('text=Enable Tracing')).not.toBeVisible()
})

test('toggle tracing on and off', async ({ page }) => {
// Expand settings
const settingsHeader = page.locator('button', { hasText: 'Tracing is' })
await settingsHeader.click()
await expect(page.locator('text=Enable Tracing')).toBeVisible()

// The Toggle component is a <label> wrapping a hidden checkbox.
// Target the checkbox within the settings panel.
const checkbox = page.locator('input[type="checkbox"]')

// Initially enabled (server starts with tracing on)
await expect(checkbox).toBeChecked()

// Click the label (parent) to toggle off
await checkbox.locator('..').click()
await expect(checkbox).not.toBeChecked()

// Click again to re-enable
await checkbox.locator('..').click()
await expect(checkbox).toBeChecked()
})

test('set max items value', async ({ page }) => {
// Expand settings
await page.locator('button', { hasText: 'Tracing is' }).click()
await expect(page.locator('text=Enable Tracing')).toBeVisible()

const maxItemsInput = page.locator('input[type="number"]')
await maxItemsInput.fill('500')
await expect(maxItemsInput).toHaveValue('500')
})

test('save shows toast', async ({ page }) => {
// Expand settings
await page.locator('button', { hasText: 'Tracing is' }).click()

// Click save
await page.locator('button', { hasText: 'Save' }).click()

// Verify toast appears
await expect(page.locator('text=Tracing settings saved')).toBeVisible({ timeout: 5_000 })
})

test('panel collapses after save when tracing is enabled', async ({ page }) => {
// Expand settings
await page.locator('button', { hasText: 'Tracing is' }).click()
await expect(page.locator('text=Enable Tracing')).toBeVisible()

// Tracing is already enabled; save
await page.locator('button', { hasText: 'Save' }).click()

// Panel should collapse
await expect(page.locator('text=Enable Tracing')).not.toBeVisible()
})

test('panel stays expanded after save when tracing is off', async ({ page }) => {
// Expand settings
await page.locator('button', { hasText: 'Tracing is' }).click()
await expect(page.locator('text=Enable Tracing')).toBeVisible()

// Toggle tracing off
await page.locator('input[type="checkbox"]').locator('..').click()

// Save
await page.locator('button', { hasText: 'Save' }).click()

// Panel should stay expanded since tracing is now disabled
await expect(page.locator('text=Enable Tracing')).toBeVisible()
})
})
64 changes: 64 additions & 0 deletions core/http/react-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions core/http/react-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint ."
"lint": "eslint .",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"react": "^19.1.0",
Expand All @@ -27,6 +29,7 @@
"@eslint/js": "^9.27.0",
"globals": "^16.1.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20"
"eslint-plugin-react-refresh": "^0.4.20",
"@playwright/test": "1.52.0"
}
}
24 changes: 24 additions & 0 deletions core/http/react-ui/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineConfig } from '@playwright/test'

export default defineConfig({
testDir: './e2e',
timeout: 30_000,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? 'html' : 'list',
use: {
baseURL: 'http://127.0.0.1:8089',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { browserName: 'chromium' },
},
],
webServer: process.env.PLAYWRIGHT_EXTERNAL_SERVER ? undefined : {
command: '../../../tests/e2e-ui/ui-test-server --mock-backend=../../../tests/e2e/mock-backend/mock-backend --port=8089',
port: 8089,
timeout: 120_000,
reuseExistingServer: !process.env.CI,
},
})
20 changes: 20 additions & 0 deletions core/http/react-ui/src/components/Modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default function Modal({ onClose, children, maxWidth = '600px' }) {
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'var(--color-modal-backdrop)', backdropFilter: 'blur(4px)',
}} onClick={onClose}>
<div style={{
background: 'var(--color-bg-secondary)',
border: '1px solid var(--color-border-subtle)',
borderRadius: 'var(--radius-lg)',
maxWidth, width: '90%', maxHeight: '80vh',
display: 'flex', flexDirection: 'column',
overflow: 'auto',
}} onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
)
}
15 changes: 15 additions & 0 deletions core/http/react-ui/src/components/SettingRow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function SettingRow({ label, description, children }) {
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: 'var(--spacing-sm) 0',
borderBottom: '1px solid var(--color-border-subtle)',
}}>
<div style={{ flex: 1, marginRight: 'var(--spacing-md)' }}>
<div style={{ fontSize: '0.875rem', fontWeight: 500 }}>{label}</div>
{description && <div style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', marginTop: 2 }}>{description}</div>}
</div>
<div style={{ flexShrink: 0 }}>{children}</div>
</div>
)
}
Loading
Loading