-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(atomic): added Generated Answer component (#3003)
* feat(headless): added generative answer feature * feat(headless): added slice tests * feat(headless): added client * feat(headless): subscribe * feat(headless): temp retry logic * feat(headless): client retry * feat(headless): exports * feat(headless): added error codes * feat(atomic): prelim components * feat(atomic): tag fix * feat(headless): actually building a controller lol * feat(headless): actually building a controller lol * feat(headless): id and error handling tweaks * feat(headless): added stream id to search event custom data * feat(headless): complete event might contain delta * feat(headless): complete event might contain delta * fix(headless): started adding payload schemas * feat(genqa): enum rename * feat(atomic): added feedback buttons * feat(headless): started adding analytics actions * feat(atomic): added citations * feat(headless): citatio interface export * feat(atomic): added localized strings * feat(headless): punctuation * feat(atomic): show when loading * feat(headless): new citations schema * feat(atomic): no score * feat(headless): removed score * feat(atomic): updated loader * feat(headless): removed oncomplete * feat(headless): added retryable state flag * feat(atomic): used analytics actions * feat(atomic): fixed loading transition * feat(headless): tests for setIsLoading * feat(atomic): citation max width * feat(headless): added more controller tests * feat(headless): removed unused analytics util * feat(headless): moved handlers to asyncthunk * feat(headless): string literal * feat(atomic): added genqa example page * feat(headless): added like and dislike to the state * feat(headless): error payload type * feat(headless): renamed and added citation seletor * feat(atomic): feedback button state * feat(headless): switched libs * feat(headless): reset error state * feat(atomic): feedback button dimensions * feat(headless): controller tests * feat(headless): changed handlers for action injection * feat(headless): controller tests * feat(headless): changed handlers for action injection * feat(headless): onpopen handler * feat(atomic): added retry button * feat(atomic): started tests * feat(headless): added arguments to fatalerror * feat(atomic): added tests * feat(headless): pr feedback * feat(atomic): added tests * feat(headless): changed client callbacks * feat(atomic): added tests * feat(headless): switch it up * feat(headless): changed accept header * feat(atomic): added tests * feat(atomic): added retry test * feat(headless): setting error before throw from onerror * feat(headless): wrong error * feat(atomic): retry button tests * feat(atomic): unknown instead of any * feat(atomic): title to citation links * feat(atomic): pr feedback * feat(atomic): css and a11y feedback implemented * feat(atomic): props interface * feat(atomic): removed smart snippets from genqa example * feat(atomic): ordered list * feat(atomic): key on top level element
- Loading branch information
Showing
19 changed files
with
964 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import {RouteAlias} from '../fixtures/fixture-common'; | ||
import {TestFixture, generateComponentHTML} from '../fixtures/test-fixture'; | ||
|
||
export const getStreamInterceptAlias = (streamId: string) => | ||
`${RouteAlias.GenQAStream}-${streamId}`; | ||
|
||
export function mockStreamResponse(streamId: string, body: unknown) { | ||
cy.intercept( | ||
{ | ||
method: 'GET', | ||
url: `**/machinelearning/streaming/${streamId}`, | ||
}, | ||
(request) => { | ||
request.reply(200, `data: ${JSON.stringify(body)} \n\n`, { | ||
'content-type': 'text/event-stream', | ||
}); | ||
} | ||
).as(getStreamInterceptAlias(streamId).substring(1)); | ||
} | ||
|
||
export function mockStreamError(streamId: string, errorCode: number) { | ||
cy.intercept( | ||
{ | ||
method: 'GET', | ||
url: `**/machinelearning/streaming/${streamId}`, | ||
}, | ||
(request) => { | ||
request.reply( | ||
errorCode, | ||
{}, | ||
{ | ||
'content-type': 'text/event-stream', | ||
} | ||
); | ||
} | ||
).as(getStreamInterceptAlias(streamId).substring(1)); | ||
} | ||
|
||
export const addGeneratedAnswer = | ||
(streamId?: string) => (fixture: TestFixture) => { | ||
const element = generateComponentHTML('atomic-generated-answer'); | ||
fixture.withElement(element).withCustomResponse((response) => { | ||
if (streamId) { | ||
response.extendedResults = { | ||
generativeQuestionAnsweringId: streamId, | ||
}; | ||
} | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export const generatedAnswerComponent = 'atomic-generated-answer'; | ||
export const GeneratedAnswerSelectors = { | ||
shadow: () => cy.get(generatedAnswerComponent).shadow(), | ||
container: () => GeneratedAnswerSelectors.shadow().find('[part="container"]'), | ||
content: () => | ||
GeneratedAnswerSelectors.shadow().find('[part="generated-content"]'), | ||
answer: () => | ||
GeneratedAnswerSelectors.shadow().find('[part="generated-text"]'), | ||
headerLabel: () => | ||
GeneratedAnswerSelectors.shadow().find('[part="header-label"]'), | ||
likeButton: () => | ||
GeneratedAnswerSelectors.shadow().find('.feedback-button.like'), | ||
dislikeButton: () => | ||
GeneratedAnswerSelectors.shadow().find('.feedback-button.dislike'), | ||
citation: () => GeneratedAnswerSelectors.shadow().find('.citation'), | ||
citationsLabel: () => | ||
GeneratedAnswerSelectors.shadow().find('.citations-label'), | ||
citationTitle: () => | ||
GeneratedAnswerSelectors.citation().find('.citation-title'), | ||
citationIndex: () => | ||
GeneratedAnswerSelectors.citation().find('.citation-index'), | ||
loader: () => GeneratedAnswerSelectors.shadow().find('.typing-indicator'), | ||
retryContainer: () => | ||
GeneratedAnswerSelectors.shadow().find('[part="retry-container"]'), | ||
retryButton: () => GeneratedAnswerSelectors.retryContainer().find('button'), | ||
}; |
174 changes: 174 additions & 0 deletions
174
packages/atomic/cypress/e2e/generated-answer.cypress.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import {TestFixture} from '../fixtures/test-fixture'; | ||
import { | ||
addGeneratedAnswer, | ||
getStreamInterceptAlias, | ||
mockStreamError, | ||
mockStreamResponse, | ||
} from './generated-answer-actions'; | ||
import {GeneratedAnswerSelectors} from './generated-answer-selectors'; | ||
|
||
describe('Generated Answer Test Suites', () => { | ||
describe('Generated Answer', () => { | ||
function setupGeneratedAnswer(streamId?: string) { | ||
new TestFixture().with(addGeneratedAnswer(streamId)).init(); | ||
} | ||
|
||
describe('when no stream ID is returned', () => { | ||
beforeEach(() => { | ||
setupGeneratedAnswer(); | ||
}); | ||
|
||
it('should not display the component', () => { | ||
GeneratedAnswerSelectors.container().should('not.exist'); | ||
}); | ||
}); | ||
|
||
describe('when a stream ID is returned', () => { | ||
describe('when a message event is received', () => { | ||
const streamId = crypto.randomUUID(); | ||
|
||
const testTextDelta = 'Some text'; | ||
const testMessagePayload = { | ||
payloadType: 'genqa.messageType', | ||
payload: JSON.stringify({ | ||
textDelta: testTextDelta, | ||
}), | ||
finishReason: 'COMPLETED', | ||
}; | ||
|
||
beforeEach(() => { | ||
mockStreamResponse(streamId, testMessagePayload); | ||
setupGeneratedAnswer(streamId); | ||
cy.wait(getStreamInterceptAlias(streamId)); | ||
}); | ||
|
||
it('should log the stream ID in the search event custom data', () => { | ||
TestFixture.getUACustomData().then((customData) => { | ||
expect(customData).to.have.property( | ||
'generativeQuestionAnsweringId', | ||
streamId | ||
); | ||
}); | ||
}); | ||
|
||
it('should display the message', () => { | ||
GeneratedAnswerSelectors.answer().should('have.text', testTextDelta); | ||
}); | ||
|
||
it('should display feedback buttons', () => { | ||
GeneratedAnswerSelectors.likeButton().should('exist'); | ||
GeneratedAnswerSelectors.dislikeButton().should('exist'); | ||
}); | ||
}); | ||
|
||
describe('when a citation event is received', () => { | ||
const streamId = crypto.randomUUID(); | ||
|
||
const testCitation = { | ||
id: 'some-id-123', | ||
title: 'Some Title', | ||
uri: 'https://www.coveo.com', | ||
permanentid: 'some-permanent-id-123', | ||
clickUri: 'https://www.coveo.com/en', | ||
}; | ||
const testMessagePayload = { | ||
payloadType: 'genqa.citationsType', | ||
payload: JSON.stringify({ | ||
citations: [testCitation], | ||
}), | ||
finishReason: 'COMPLETED', | ||
}; | ||
|
||
beforeEach(() => { | ||
mockStreamResponse(streamId, testMessagePayload); | ||
setupGeneratedAnswer(streamId); | ||
cy.wait(getStreamInterceptAlias(streamId)); | ||
}); | ||
|
||
it('should display the citation link', () => { | ||
GeneratedAnswerSelectors.citationsLabel().should( | ||
'have.text', | ||
'Learn more' | ||
); | ||
GeneratedAnswerSelectors.citationTitle().should( | ||
'have.text', | ||
testCitation.title | ||
); | ||
GeneratedAnswerSelectors.citationIndex().should('have.text', '1'); | ||
GeneratedAnswerSelectors.citation().should( | ||
'have.attr', | ||
'href', | ||
testCitation.clickUri | ||
); | ||
}); | ||
}); | ||
|
||
describe('when an error event is received', () => { | ||
const streamId = crypto.randomUUID(); | ||
|
||
const testErrorPayload = { | ||
finishReason: 'ERROR', | ||
errorMessage: 'An error message', | ||
errorCode: 500, | ||
}; | ||
|
||
beforeEach(() => { | ||
mockStreamResponse(streamId, testErrorPayload); | ||
setupGeneratedAnswer(streamId); | ||
cy.wait(getStreamInterceptAlias(streamId)); | ||
}); | ||
|
||
it('should not display the component', () => { | ||
GeneratedAnswerSelectors.container().should('not.exist'); | ||
}); | ||
}); | ||
|
||
describe('when the stream connection fails', () => { | ||
const streamId = crypto.randomUUID(); | ||
|
||
describe('Non-retryable error (4XX)', () => { | ||
beforeEach(() => { | ||
mockStreamError(streamId, 406); | ||
setupGeneratedAnswer(streamId); | ||
cy.wait(getStreamInterceptAlias(streamId)); | ||
}); | ||
|
||
it('should not show the component', () => { | ||
GeneratedAnswerSelectors.container().should('not.exist'); | ||
}); | ||
}); | ||
|
||
describe('Retryable error', () => { | ||
[500, 429].forEach((errorCode) => { | ||
describe(`${errorCode} error`, () => { | ||
beforeEach(() => { | ||
Cypress.on('uncaught:exception', () => false); | ||
mockStreamError(streamId, 500); | ||
setupGeneratedAnswer(streamId); | ||
cy.wait(getStreamInterceptAlias(streamId)); | ||
}); | ||
|
||
it('should retry the stream 3 times then offer a retry button', () => { | ||
for (let times = 0; times < 3; times++) { | ||
GeneratedAnswerSelectors.container().should('not.exist'); | ||
|
||
cy.wait(getStreamInterceptAlias(streamId)); | ||
} | ||
|
||
const retryAlias = '@retrySearch'; | ||
cy.intercept({ | ||
method: 'POST', | ||
url: '**/rest/search/v2?*', | ||
}).as(retryAlias.substring(1)); | ||
|
||
GeneratedAnswerSelectors.retryButton().click(); | ||
|
||
cy.wait(retryAlias); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.