Skip to content

Commit 09c3d6f

Browse files
authored
Merge branch 'main' into updatecli_main_bfbda0570cfbf1ebee5ba4801497a4b00fe1289653863b5c09f26db4b8c67c6e
2 parents 538d2fd + a9fe08e commit 09c3d6f

File tree

8 files changed

+9889
-4180
lines changed

8 files changed

+9889
-4180
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
build-lambda:
3636
uses: ./.github/workflows/build-link-index-updater-lambda.yml
3737

38-
lint:
38+
npm:
3939
runs-on: ubuntu-latest
4040
defaults:
4141
run:
@@ -57,6 +57,12 @@ jobs:
5757

5858
- name: Format
5959
run: npm run fmt:check
60+
61+
- name: Build
62+
run: npm run build
63+
64+
- name: Test
65+
run: npm run test
6066

6167

6268
build:
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import { AskAiAnswer } from './AskAiAnswer'
2+
import { LlmGatewayMessage, useLlmGateway } from './useLlmGateway'
3+
import { render, screen } from '@testing-library/react'
4+
import userEvent from '@testing-library/user-event'
5+
import * as React from 'react'
6+
import { act } from 'react'
7+
8+
const mockUseLlmGateway = jest.mocked(useLlmGateway)
9+
10+
const mockSendQuestion = jest.fn(() => Promise.resolve())
11+
const mockRetry = jest.fn()
12+
const mockAbort = jest.fn()
13+
14+
jest.mock('./search.store', () => ({
15+
useAskAiTerm: jest.fn(() => 'What is Elasticsearch?'),
16+
}))
17+
18+
jest.mock('./useLlmGateway', () => ({
19+
useLlmGateway: jest.fn(() => ({
20+
messages: [],
21+
error: null,
22+
abort: mockAbort,
23+
retry: mockRetry,
24+
sendQuestion: mockSendQuestion,
25+
})),
26+
}))
27+
28+
// Mock uuid
29+
jest.mock('uuid', () => ({
30+
v4: jest.fn(() => 'mock-uuid-123'),
31+
}))
32+
33+
describe('AskAiAnswer Component', () => {
34+
beforeEach(() => {
35+
jest.clearAllMocks()
36+
})
37+
38+
describe('Initial Loading State', () => {
39+
test('should show loading spinner when no messages are present', () => {
40+
// Arrange
41+
mockUseLlmGateway.mockReturnValue({
42+
messages: [],
43+
error: null,
44+
retry: mockRetry,
45+
sendQuestion: mockSendQuestion,
46+
abort: mockAbort,
47+
})
48+
49+
// Act
50+
render(<AskAiAnswer />)
51+
52+
// Assert
53+
const loadingSpinner = screen.getByRole('progressbar')
54+
expect(loadingSpinner).toBeInTheDocument()
55+
expect(screen.getByText('Generating...')).toBeInTheDocument()
56+
})
57+
})
58+
59+
describe('Message Display', () => {
60+
test('should display AI message content correctly', () => {
61+
// Arrange
62+
const mockMessages: LlmGatewayMessage[] = [
63+
{
64+
id: 'some-id-1',
65+
timestamp: 0,
66+
type: 'ai_message',
67+
data: {
68+
content:
69+
'Elasticsearch is a distributed search engine...',
70+
},
71+
},
72+
{
73+
id: 'some-id-2',
74+
timestamp: 0,
75+
type: 'ai_message_chunk',
76+
data: {
77+
content: ' It provides real-time search capabilities.',
78+
},
79+
},
80+
]
81+
82+
mockUseLlmGateway.mockReturnValue({
83+
messages: mockMessages,
84+
error: null,
85+
retry: mockRetry,
86+
sendQuestion: mockSendQuestion,
87+
abort: mockAbort,
88+
})
89+
90+
// Act
91+
render(<AskAiAnswer />)
92+
93+
// Assert
94+
const expectedContent =
95+
'Elasticsearch is a distributed search engine... It provides real-time search capabilities.'
96+
expect(screen.getByText(expectedContent)).toBeInTheDocument()
97+
})
98+
})
99+
100+
describe('Error State', () => {
101+
test('should display error message when there is an error', () => {
102+
// Arrange
103+
mockUseLlmGateway.mockReturnValue({
104+
messages: [],
105+
error: new Error('Network error'),
106+
retry: mockRetry,
107+
sendQuestion: mockSendQuestion,
108+
abort: mockAbort,
109+
})
110+
111+
// Act
112+
render(<AskAiAnswer />)
113+
114+
// Assert
115+
expect(
116+
screen.getByText('Sorry, there was an error')
117+
).toBeInTheDocument()
118+
expect(
119+
screen.getByText(
120+
'The Elastic Docs AI Assistant encountered an error. Please try again.'
121+
)
122+
).toBeInTheDocument()
123+
})
124+
})
125+
126+
describe('Finished State with Feedback Buttons', () => {
127+
test('should show feedback buttons when answer is finished', () => {
128+
// Arrange
129+
let onMessageCallback: (
130+
message: LlmGatewayMessage
131+
) => void = () => {}
132+
133+
const mockMessages: LlmGatewayMessage[] = [
134+
{
135+
id: 'some-id-1',
136+
timestamp: 1,
137+
type: 'ai_message',
138+
data: {
139+
content: 'Here is your answer about Elasticsearch.',
140+
},
141+
},
142+
]
143+
144+
mockUseLlmGateway.mockImplementation(({ onMessage }) => {
145+
onMessageCallback = onMessage!
146+
return {
147+
messages: mockMessages,
148+
error: null,
149+
retry: mockRetry,
150+
sendQuestion: mockSendQuestion,
151+
abort: mockAbort,
152+
}
153+
})
154+
155+
// Act
156+
render(<AskAiAnswer />)
157+
158+
// Simulate the component receiving an 'agent_end' message to finish loading
159+
act(() => {
160+
onMessageCallback({
161+
type: 'agent_end',
162+
id: 'some-id',
163+
timestamp: 12345,
164+
data: {},
165+
})
166+
})
167+
168+
// Assert
169+
expect(
170+
screen.getByLabelText('This answer was helpful')
171+
).toBeInTheDocument()
172+
expect(
173+
screen.getByLabelText('This answer was not helpful')
174+
).toBeInTheDocument()
175+
expect(
176+
screen.getByLabelText('Request a new answer')
177+
).toBeInTheDocument()
178+
})
179+
180+
test('should call retry function when refresh button is clicked', async () => {
181+
// Arrange
182+
const user = userEvent.setup()
183+
let onMessageCallback: (
184+
message: LlmGatewayMessage
185+
) => void = () => {}
186+
187+
const mockMessages: LlmGatewayMessage[] = [
188+
{
189+
id: 'some-id-1',
190+
timestamp: 12345,
191+
type: 'ai_message',
192+
data: { content: 'Here is your answer.' },
193+
},
194+
]
195+
196+
mockUseLlmGateway.mockImplementation(({ onMessage }) => {
197+
onMessageCallback = onMessage!
198+
return {
199+
messages: mockMessages,
200+
error: null,
201+
retry: mockRetry,
202+
sendQuestion: mockSendQuestion,
203+
abort: mockAbort,
204+
}
205+
})
206+
207+
render(<AskAiAnswer />)
208+
209+
// Simulate finished state
210+
act(() => {
211+
onMessageCallback({
212+
type: 'agent_start',
213+
id: 'some-id',
214+
timestamp: 12345,
215+
data: { input: {}, thread: {} },
216+
})
217+
onMessageCallback({
218+
type: 'agent_end',
219+
id: 'some-id',
220+
timestamp: 12346,
221+
data: {},
222+
})
223+
})
224+
225+
// Act
226+
const refreshButton = screen.getByLabelText('Request a new answer')
227+
228+
await act(async () => {
229+
await user.click(refreshButton)
230+
})
231+
232+
// Assert
233+
expect(mockRetry).toHaveBeenCalledTimes(1)
234+
})
235+
})
236+
237+
describe('Question Sending', () => {
238+
test('should send question on component mount', () => {
239+
// Arrange
240+
mockUseLlmGateway.mockReturnValue({
241+
messages: [],
242+
error: null,
243+
retry: mockRetry,
244+
sendQuestion: mockSendQuestion,
245+
abort: mockAbort,
246+
})
247+
248+
// Act
249+
render(<AskAiAnswer />)
250+
251+
// Assert
252+
expect(mockSendQuestion).toHaveBeenCalledWith(
253+
'What is Elasticsearch?'
254+
)
255+
})
256+
})
257+
})

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const SearchOrAskAiModal = () => {
1515
const searchTerm = useSearchTerm()
1616
const askAiTerm = useAskAiTerm()
1717
const { setSearchTerm, submitAskAiTerm } = useSearchActions()
18-
18+
1919
return (
2020
<>
2121
<EuiFieldSearch
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module.exports = {
2+
testEnvironment: 'jsdom',
3+
setupFilesAfterEnv: ['<rootDir>/setupTests.ts'],
4+
transform: {
5+
'^.+\\.(ts|tsx)$': [
6+
'babel-jest',
7+
{
8+
presets: [
9+
['@babel/preset-env', { targets: { node: 'current' } }],
10+
['@babel/preset-react', { runtime: 'automatic' }],
11+
'@babel/preset-typescript',
12+
],
13+
},
14+
],
15+
'^.+\\.(js|jsx)$': [
16+
'babel-jest',
17+
{
18+
presets: [
19+
['@babel/preset-env', { targets: { node: 'current' } }],
20+
['@babel/preset-react', { runtime: 'automatic' }],
21+
],
22+
},
23+
],
24+
},
25+
transformIgnorePatterns: [],
26+
reporters: [['github-actions', { silent: false }], 'summary'],
27+
}

0 commit comments

Comments
 (0)