test(core): split and enhance copilot e2e tests (#11007)

### TL;DR

Split and enhance copilot e2e tests.

### What Changed

#### Tests Structure

The e2e tests are organized into the following categories:

1. **Basic Tests (`/basic`)**: Tests for verifying core AI capabilities including feature onboarding, authorization workflows, and basic chat interactions.
2. **Chat Interaction Tests (`/chat-with`)**: Tests for verifying the AI's interaction with various ​object types, such as attachments, images, text content, Edgeless elements, etc.
3. **AI Action Tests (`/ai-action`)**: Tests for verifying the AI's actions, such as text translation, gramma correction, etc.
4. **Insertion Tests (`/insertion`)**: Tests for verifying answer insertion functionality.

#### Tests Writing

Writing a copilot test cases is easier and clear

e.g.
```ts
test('support chat with specified doc', async ({ page, utils }) => {
  // Initialize the doc
  await focusDocTitle(page);
  await page.keyboard.insertText('Test Doc');
  await page.keyboard.press('Enter');
  await page.keyboard.insertText('EEee is a cute cat');

  await utils.chatPanel.chatWithDoc(page, 'Test Doc');

  await utils.chatPanel.makeChat(page, 'What is EEee?');
  await utils.chatPanel.waitForHistory(page, [
    {
      role: 'user',
      content: 'What is EEee?',
    },
    {
      role: 'assistant',
      status: 'success',
    },
  ]);

  const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
  expect(content).toMatch(/EEee/);
});
```

#### Summary

||Cases|
|------|----|
|Before|19||
|After|151||

> Close BS-2769
This commit is contained in:
yoyoyohamapi
2025-03-29 03:41:09 +00:00
parent a709ed2ef1
commit 317d3e7ea6
94 changed files with 4898 additions and 1225 deletions

View File

@@ -49,6 +49,7 @@ import {
export const translateSubItem: AISubItemConfig[] = translateLangs.map(lang => {
return {
type: lang,
testId: `action-translate-${lang}`,
handler: actionToHandler('translate', AIStarIconWithAnimation, { lang }),
};
});
@@ -56,6 +57,7 @@ export const translateSubItem: AISubItemConfig[] = translateLangs.map(lang => {
export const toneSubItem: AISubItemConfig[] = textTones.map(tone => {
return {
type: tone,
testId: `action-change-tone-${tone.toLowerCase()}`,
handler: actionToHandler('changeTone', AIStarIconWithAnimation, { tone }),
};
});
@@ -66,6 +68,7 @@ export function createImageFilterSubItem(
return imageFilterStyles.map(style => {
return {
type: style,
testId: `action-image-filter-${style.toLowerCase().replace(' ', '-')}`,
handler: actionToHandler(
'filterImage',
AIImageIconWithAnimation,
@@ -84,6 +87,7 @@ export function createImageProcessingSubItem(
return imageProcessingTypes.map(type => {
return {
type,
testId: `action-image-processing-${type.toLowerCase().replace(' ', '-')}`,
handler: actionToHandler(
'processImage',
AIImageIconWithAnimation,
@@ -146,36 +150,42 @@ const EditAIGroup: AIItemGroupConfig = {
items: [
{
name: 'Translate to',
testId: 'action-translate',
icon: LanguageIcon(),
showWhen: textBlockShowWhen,
subItem: translateSubItem,
},
{
name: 'Change tone to',
testId: 'action-change-tone',
icon: ToneIcon(),
showWhen: textBlockShowWhen,
subItem: toneSubItem,
},
{
name: 'Improve writing',
testId: 'action-improve-writing',
icon: ImproveWritingIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('improveWriting', AIStarIconWithAnimation),
},
{
name: 'Make it longer',
testId: 'action-make-it-longer',
icon: LongerIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('makeLonger', AIStarIconWithAnimation),
},
{
name: 'Make it shorter',
testId: 'action-make-it-shorter',
icon: ShorterIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('makeShorter', AIStarIconWithAnimation),
},
{
name: 'Continue writing',
testId: 'action-continue-writing',
icon: PenIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('continueWriting', AIPenIconWithAnimation),
@@ -188,30 +198,35 @@ const DraftAIGroup: AIItemGroupConfig = {
items: [
{
name: 'Write an article about this',
testId: 'action-write-article',
icon: PenIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('writeArticle', AIPenIconWithAnimation),
},
{
name: 'Write a tweet about this',
testId: 'action-write-twitter-post',
icon: PenIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('writeTwitterPost', AIPenIconWithAnimation),
},
{
name: 'Write a poem about this',
testId: 'action-write-poem',
icon: PenIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('writePoem', AIPenIconWithAnimation),
},
{
name: 'Write a blog post about this',
testId: 'action-write-blog-post',
icon: PenIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('writeBlogPost', AIPenIconWithAnimation),
},
{
name: 'Brainstorm ideas about this',
testId: 'action-brainstorm',
icon: PenIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('brainstorm', AIPenIconWithAnimation),
@@ -224,36 +239,42 @@ const ReviewWIthAIGroup: AIItemGroupConfig = {
items: [
{
name: 'Fix spelling',
testId: 'action-fix-spelling',
icon: DoneIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('fixSpelling', AIStarIconWithAnimation),
},
{
name: 'Fix grammar',
testId: 'action-fix-grammar',
icon: DoneIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('improveGrammar', AIStarIconWithAnimation),
},
{
name: 'Explain this image',
testId: 'action-explain-image',
icon: PenIcon(),
showWhen: imageBlockShowWhen,
handler: actionToHandler('explainImage', AIStarIconWithAnimation),
},
{
name: 'Explain this code',
testId: 'action-explain-code',
icon: ExplainIcon(),
showWhen: codeBlockShowWhen,
handler: actionToHandler('explainCode', AIStarIconWithAnimation),
},
{
name: 'Check code error',
testId: 'action-check-code-error',
icon: ExplainIcon(),
showWhen: codeBlockShowWhen,
handler: actionToHandler('checkCodeErrors', AIStarIconWithAnimation),
},
{
name: 'Explain selection',
testId: 'action-explain-selection',
icon: SelectionIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('explain', AIStarIconWithAnimation),
@@ -266,12 +287,14 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
items: [
{
name: 'Summarize',
testId: 'action-summarize',
icon: PenIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('summary', AIPenIconWithAnimation),
},
{
name: 'Generate headings',
testId: 'action-generate-headings',
icon: PenIcon(),
beta: true,
handler: actionToHandler('createHeadings', AIPenIconWithAnimation),
@@ -293,24 +316,28 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
},
{
name: 'Generate an image',
testId: 'action-generate-image',
icon: ImageIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('createImage', AIImageIconWithAnimation),
},
{
name: 'Generate outline',
testId: 'action-generate-outline',
icon: PenIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('writeOutline', AIPenIconWithAnimation),
},
{
name: 'Brainstorm ideas with mind map',
testId: 'action-brainstorm-mindmap',
icon: MindmapIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('brainstormMindmap', AIPenIconWithAnimation),
},
{
name: 'Generate presentation',
testId: 'action-generate-presentation',
icon: PresentationIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('createSlides', AIPresentationIconWithAnimation),
@@ -318,6 +345,7 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
},
{
name: 'Make it real',
testId: 'action-make-it-real',
icon: MakeItRealIcon(),
beta: true,
showWhen: textBlockShowWhen,
@@ -325,6 +353,7 @@ const GenerateWithAIGroup: AIItemGroupConfig = {
},
{
name: 'Find actions',
testId: 'action-find-actions',
icon: SearchIcon(),
showWhen: textBlockShowWhen,
handler: actionToHandler('findActions', AIStarIconWithAnimation),
@@ -338,6 +367,7 @@ const OthersAIGroup: AIItemGroupConfig = {
items: [
{
name: 'Continue with AI',
testId: 'action-continue-with-ai',
icon: CommentIcon(),
handler: host => {
const panel = getAIPanelWidget(host);
@@ -366,6 +396,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
items: [
{
name: 'Explain this image',
testId: 'action-explain-image',
icon: ImageIcon(),
showWhen: () => true,
handler: actionToHandler(
@@ -382,6 +413,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
items: [
{
name: 'Generate an image',
testId: 'action-generate-image',
icon: ImageIcon(),
showWhen: () => true,
handler: actionToHandler(
@@ -393,6 +425,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
},
{
name: 'Image processing',
testId: 'action-image-processing',
icon: ImageIcon(),
showWhen: () => true,
subItem: createImageProcessingSubItem(blockActionTrackerOptions),
@@ -401,6 +434,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
},
{
name: 'AI image filter',
testId: 'action-ai-image-filter',
icon: ImproveWritingIcon(),
showWhen: () => true,
subItem: createImageFilterSubItem(blockActionTrackerOptions),
@@ -409,6 +443,7 @@ export function buildAIImageItemGroups(): AIItemGroupConfig[] {
},
{
name: 'Generate a caption',
testId: 'action-generate-caption',
icon: PenIcon(),
showWhen: () => true,
beta: true,
@@ -432,6 +467,7 @@ export function buildAICodeItemGroups(): AIItemGroupConfig[] {
items: [
{
name: 'Explain this code',
testId: 'action-explain-code',
icon: ExplainIcon(),
showWhen: () => true,
handler: actionToHandler(
@@ -443,6 +479,7 @@ export function buildAICodeItemGroups(): AIItemGroupConfig[] {
},
{
name: 'Check code error',
testId: 'action-check-code-error',
icon: ExplainIcon(),
showWhen: () => true,
handler: actionToHandler(

View File

@@ -462,7 +462,6 @@ export function noteBlockOrTextShowWhen(
host: EditorHost
) {
const selected = getCopilotSelectedElems(host);
return selected.some(
el =>
el instanceof NoteBlockModel ||

View File

@@ -85,6 +85,7 @@ export function discard(
return {
name: 'Discard',
icon: DeleteIcon(),
testId: 'answer-discard',
showWhen: () => !!panel.answer,
handler: () => {
panel.discard();
@@ -96,6 +97,7 @@ export function retry(panel: AffineAIPanelWidget): AIItemConfig {
return {
name: 'Retry',
icon: ResetIcon(),
testId: 'answer-retry',
handler: () => {
reportResponse('result:retry');
panel.generate();
@@ -123,6 +125,7 @@ export function createInsertItems<T extends keyof BlockSuitePresets.AIActions>(
icon: html`<div style=${styleMap({ height: '20px', width: '20px' })}>
${LightLoadingIcon}
</div>`,
testId: 'answer-insert-below-loading',
showWhen: () => {
const panel = getAIPanelWidget(host);
const data = ctx.get();
@@ -137,6 +140,8 @@ export function createInsertItems<T extends keyof BlockSuitePresets.AIActions>(
{
name: buttonText,
icon: InsertBelowIcon(),
testId:
buttonText === 'Replace' ? 'answer-replace' : `answer-insert-below`,
showWhen: () => {
const panel = getAIPanelWidget(host);
const data = ctx.get();
@@ -191,6 +196,7 @@ export function asCaption<T extends keyof BlockSuitePresets.AIActions>(
return {
name: 'Use as caption',
icon: PenIcon(),
testId: 'answer-use-as-caption',
showWhen: () => {
const panel = getAIPanelWidget(host);
return id === 'generateCaption' && !!panel.answer;
@@ -553,9 +559,11 @@ export function actionToResponse<T extends keyof BlockSuitePresets.AIActions>(
responses: [
{
name: 'Response',
testId: 'answer-responses',
items: [
{
name: 'Continue in chat',
testId: 'answer-continue-in-chat',
icon: ChatWithAiIcon({}),
handler: () => {
reportResponse('result:continue-in-chat');

View File

@@ -53,6 +53,7 @@ function asCaption<T extends keyof BlockSuitePresets.AIActions>(
return {
name: 'Use as caption',
icon: PenIcon(),
testId: 'answer-use-as-caption',
showWhen: () => {
const panel = getAIPanelWidget(host);
return id === 'generateCaption' && !!panel.answer;
@@ -79,6 +80,7 @@ function createNewNote(host: EditorHost): AIItemConfig {
return {
name: 'Create new note',
icon: PageIcon(),
testId: 'answer-create-new-note',
showWhen: () => {
const panel = getAIPanelWidget(host);
return !!panel.answer && isInsideEdgelessEditor(host);
@@ -147,9 +149,11 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
return [
{
name: 'Response',
testId: 'answer-responses',
items: [
{
name: 'Insert below',
testId: 'answer-insert-below',
icon: InsertBelowIcon(),
showWhen: () =>
!!panel.answer && (!id || !INSERT_ABOVE_ACTIONS.includes(id)),
@@ -161,6 +165,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
},
{
name: 'Insert above',
testId: 'answer-insert-above',
icon: InsertTopIcon(),
showWhen: () =>
!!panel.answer && !!id && INSERT_ABOVE_ACTIONS.includes(id),
@@ -173,6 +178,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
asCaption(host, id),
{
name: 'Replace selection',
testId: 'answer-replace',
icon: ReplaceIcon(),
showWhen: () =>
!!panel.answer && !EXCLUDING_REPLACE_ACTIONS.includes(id),
@@ -187,10 +193,12 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
},
{
name: '',
testId: 'answer-common-responses',
items: [
{
name: 'Continue in chat',
icon: ChatWithAiIcon(),
testId: 'answer-continue-in-chat',
handler: () => {
reportResponse('result:continue-in-chat');
AIProvider.slots.requestOpenWithChat.next({ host });
@@ -200,6 +208,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
{
name: 'Regenerate',
icon: ResetIcon(),
testId: 'answer-regenerate',
handler: () => {
reportResponse('result:retry');
panel.generate();
@@ -208,6 +217,7 @@ function buildPageResponseConfig<T extends keyof BlockSuitePresets.AIActions>(
{
name: 'Discard',
icon: DeleteIcon(),
testId: 'answer-discard',
handler: () => {
panel.discard();
},
@@ -225,6 +235,7 @@ export function buildErrorResponseConfig(panel: AffineAIPanelWidget) {
{
name: 'Retry',
icon: ResetIcon(),
testId: 'error-retry',
showWhen: () => true,
handler: () => {
reportResponse('result:retry');
@@ -234,6 +245,7 @@ export function buildErrorResponseConfig(panel: AffineAIPanelWidget) {
{
name: 'Discard',
icon: DeleteIcon(),
testId: 'error-discard',
showWhen: () => !!panel.answer,
handler: () => {
panel.discard();

View File

@@ -142,6 +142,7 @@ export class ActionWrapper extends WithDisposable(LitElement) {
<slot></slot>
<div
class="action-name"
data-testid="action-name"
@click=${() => (this.promptShow = !this.promptShow)}
>
${icons[item.action] ? icons[item.action] : DoneIcon()}
@@ -152,22 +153,27 @@ export class ActionWrapper extends WithDisposable(LitElement) {
</div>
${this.promptShow
? html`
<div class="answer-prompt">
<div class="answer-prompt" data-testid="answer-prompt">
<div class="subtitle">Answer</div>
${HISTORY_IMAGE_ACTIONS.includes(item.action)
? images &&
html`<chat-content-images
.images=${images}
data-testid="generated-image"
></chat-content-images>`
: nothing}
${answer
? createTextRenderer(this.host, { customHeading: true })(answer)
? createTextRenderer(this.host, {
customHeading: true,
testId: 'chat-message-action-answer',
})(answer)
: nothing}
${originalText
? html`<div class="subtitle prompt">Prompt</div>
${createTextRenderer(this.host, { customHeading: true })(
item.messages[0].content + originalText
)}`
${createTextRenderer(this.host, {
customHeading: true,
testId: 'chat-message-action-prompt',
})(item.messages[0].content + originalText)}`
: nothing}
</div>
`

View File

@@ -1,6 +1,3 @@
import './action-wrapper';
import '../content/images';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
@@ -27,7 +24,10 @@ export class ActionImageToText extends WithDisposable(ShadowlessElement) {
})}
>
${answer
? html`<chat-content-images .images=${answer}></chat-content-images>`
? html`<chat-content-images
data-testid="original-images"
.images=${answer}
></chat-content-images>`
: nothing}
</div>
</action-wrapper>`;

View File

@@ -17,13 +17,19 @@ export class ActionImage extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'action-image';
protected override render() {
const images = this.item.messages[0].attachments;
return html`<action-wrapper .host=${this.host} .item=${this.item}>
<div style=${styleMap({ marginBottom: '12px' })}>
${images
? html`<chat-content-images .images=${images}></chat-content-images>`
? html`<chat-content-images
.images=${images}
data-testid="original-image"
></chat-content-images>`
: nothing}
</div>
</action-wrapper>`;

View File

@@ -54,6 +54,7 @@ export class ActionText extends WithDisposable(LitElement) {
border: isCode ? 'none' : '1px solid var(--affine-border-color)',
})}
class="original-text"
data-testid="original-text"
>
${createTextRenderer(this.host, {
customHeading: true,

View File

@@ -47,6 +47,9 @@ export class AILoading extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor stopGenerating!: () => void;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'ai-loading';
override render() {
return html`
<div class="generating-tip">

View File

@@ -99,6 +99,9 @@ export class ChatPanelChips extends SignalWatcher(
@property({ attribute: false })
accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-panel-chips';
@query('.add-button')
accessor addButton!: HTMLDivElement;
@@ -137,7 +140,11 @@ export class ChatPanelChips extends SignalWatcher(
const chips = isCollapsed ? allChips.slice(0, 1) : allChips;
return html`<div class="chips-wrapper">
<div class="add-button" @click=${this._toggleAddDocMenu}>
<div
class="add-button"
data-testid="chat-panel-with-button"
@click=${this._toggleAddDocMenu}
>
${PlusIcon()}
</div>
${repeat(

View File

@@ -220,6 +220,9 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-panel-input-container';
private get _isNetworkActive() {
return (
!!this.networkSearchConfig.visible.value &&
@@ -335,7 +338,10 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
`
: nothing}
${this.chatContextValue.quote
? html`<div class="chat-selection-quote">
? html`<div
class="chat-selection-quote"
data-testid="chat-selection-quote"
>
${repeat(
getFirstTwoLines(this.chatContextValue.quote),
line => line,
@@ -420,6 +426,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
: nothing}
${images.length < MaximumImageCount
? html`<div
data-testid="chat-panel-input-image-upload"
class="image-upload"
aria-disabled=${uploadDisabled}
@click=${uploadDisabled ? undefined : this._uploadImageFiles}
@@ -434,6 +441,7 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
this.updateContext({ status: 'success' });
reportResponse('aborted:stop');
}}
data-testid="chat-panel-stop"
>
${ChatAbortIcon}
</div>`

View File

@@ -31,7 +31,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
position: relative;
}
.chat-panel-messages {
.chat-panel-messages-container {
display: flex;
flex-direction: column;
gap: 24px;
@@ -157,9 +157,16 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor previewSpecBuilder!: SpecBuilder;
@query('.chat-panel-messages')
@query('.chat-panel-messages-container')
accessor messagesContainer: HTMLDivElement | null = null;
@property({
type: String,
attribute: 'data-testid',
reflect: true,
})
accessor testId = 'chat-panel-messages';
getScrollContainer(): HTMLDivElement | null {
return this.messagesContainer;
}
@@ -168,12 +175,13 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
return this.isLoading ||
!this.host?.doc.get(FeatureFlagService).getFlag('enable_ai_onboarding')
? nothing
: html`<div class="onboarding-wrapper">
: html`<div class="onboarding-wrapper" data-testid="ai-onboarding">
${repeat(
AIPreloadConfig,
config => config.text,
config => {
return html`<div
data-testid=${config.testId}
@click=${() => config.handler()}
class="onboarding-item"
>
@@ -220,7 +228,8 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
return html`
<div
class="chat-panel-messages"
class="chat-panel-messages-container"
data-testid="chat-panel-messages-container"
@scroll=${() => this._debouncedOnScroll()}
>
${filteredItems.length === 0
@@ -232,8 +241,12 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
)}
<div class="messages-placeholder-title" data-loading=${isLoading}>
${this.isLoading
? 'AFFiNE AI is loading history...'
: 'What can I help you with?'}
? html`<span data-testid="chat-panel-loading-state"
>AFFiNE AI is loading history...</span
>`
: html`<span data-testid="chat-panel-empty-state"
>What can I help you with?</span
>`}
</div>
${this._renderAIOnboarding()}
</div> `
@@ -268,7 +281,11 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
)}
</div>
${showDownIndicator && filteredItems.length > 0
? html`<div class="down-indicator" @click=${this._onDownIndicatorClick}>
? html`<div
data-testid="chat-panel-scroll-down-indicator"
class="down-indicator"
@click=${this._onDownIndicatorClick}
>
${ArrowDownIcon()}
</div>`
: nothing}

View File

@@ -44,6 +44,8 @@ export type MenuItem = {
name: string | TemplateResult<1>;
icon: TemplateResult<1>;
action: MenuAction;
suffix?: string | TemplateResult<1>;
testId?: string;
};
export type MenuAction = () => Promise<void> | void;
@@ -140,6 +142,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
{
key: 'tags',
name: 'Tags',
testId: 'ai-chat-with-tags',
icon: TagsIcon(),
action: () => {
this._toggleMode(AddPopoverMode.Tags);
@@ -148,6 +151,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
{
key: 'collections',
name: 'Collections',
testId: 'ai-chat-with-collections',
icon: CollectionsIcon(),
action: () => {
this._toggleMode(AddPopoverMode.Collections);
@@ -176,6 +180,7 @@ export class ChatPanelAddPopover extends SignalWatcher(
{
key: 'files',
name: 'Upload files (pdf, txt, csv)',
testId: 'ai-chat-with-files',
icon: UploadIcon(),
action: this._addFileChip,
},
@@ -330,13 +335,14 @@ export class ChatPanelAddPopover extends SignalWatcher(
${repeat(
items,
item => item.key,
({ key, name, icon, action }, idx) => {
({ key, name, icon, action, testId }, idx) => {
const curIdx = startIndex + idx;
return html`<icon-button
width="280px"
height="30px"
data-id=${key}
data-index=${curIdx}
data-testid=${testId}
.text=${name}
hover=${this._activatedIndex === curIdx}
@click=${() => action()?.catch(console.error)}

View File

@@ -37,6 +37,9 @@ export class ChatContentPureText extends ShadowlessElement {
@property({ attribute: false })
accessor text: string = '';
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-content-pure-text';
protected override render() {
return this.text.length > 0
? html`<div class="chat-content-pure-text">${this.text}</div>`

View File

@@ -16,6 +16,9 @@ export class ChatMessageAction extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor item!: ChatAction;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-message-action';
renderHeader() {
return html`
<div class="user-info">

View File

@@ -34,7 +34,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor isLast: boolean = false;
@property({ attribute: false })
@property({ attribute: 'data-status', reflect: true })
accessor status: string = 'idle';
@property({ attribute: false })
@@ -49,6 +49,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor retry!: () => void;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-message-assistant';
renderHeader() {
const isWithDocs =
'content' in this.item &&

View File

@@ -31,6 +31,9 @@ export class ChatMessageUser extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor item!: ChatMessage;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-message-user';
renderContent() {
const { item } = this;
@@ -41,7 +44,7 @@ export class ChatMessageUser extends WithDisposable(ShadowlessElement) {
.images=${item.attachments}
></chat-content-images>`
: nothing}
<div class="text-content-wrapper">
<div class="text-content-wrapper" data-test-id="chat-content-user-text">
<chat-content-pure-text .text=${item.content}></chat-content-pure-text>
</div>
`;

View File

@@ -17,6 +17,7 @@ export const AIPreloadConfig = [
{
icon: LanguageIcon(),
text: 'Read a foreign language article with AI',
testId: 'read-foreign-language-article-with-ai',
handler: () => {
AIProvider.slots.requestInsertTemplate.next({
template: readAforeign,
@@ -27,6 +28,7 @@ export const AIPreloadConfig = [
{
icon: MindmapIcon(),
text: 'Tidy an article with AI MindMap Action',
testId: 'tidy-an-article-with-ai-mindmap-action',
handler: () => {
AIProvider.slots.requestInsertTemplate.next({
template: TidyMindMapV3,
@@ -37,6 +39,7 @@ export const AIPreloadConfig = [
{
icon: ImageIcon(),
text: 'Add illustrations to the article',
testId: 'add-illustrations-to-the-article',
handler: () => {
AIProvider.slots.requestInsertTemplate.next({
template: redHat,
@@ -47,6 +50,7 @@ export const AIPreloadConfig = [
{
icon: PenIcon(),
text: 'Complete writing with AI',
testId: 'complete-writing-with-ai',
handler: () => {
AIProvider.slots.requestInsertTemplate.next({
template: completeWritingWithAI,
@@ -57,6 +61,7 @@ export const AIPreloadConfig = [
{
icon: SendIcon(),
text: 'Freely communicate with AI',
testId: 'freely-communicate-with-ai',
handler: () => {
AIProvider.slots.requestInsertTemplate.next({
template: freelyCommunicateWithAI,

View File

@@ -87,6 +87,7 @@ export class AIItemList extends WithDisposable(LitElement) {
createLitPortal({
template: html`<ai-sub-item-list
data-testid=${item.testId ? item.testId + '-menu' : ''}
.item=${item}
.host=${this.host}
.onClick=${this.onClick}
@@ -141,6 +142,9 @@ export class AIItemList extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor onClick: (() => void) | undefined = undefined;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'ai-item-list';
}
declare global {

View File

@@ -23,8 +23,10 @@ export class AIItem extends WithDisposable(LitElement) {
override render() {
const { item } = this;
const className = item.name.split(' ').join('-').toLocaleLowerCase();
const testId = item.testId;
return html`<div
data-testid=${testId}
class="menu-item ${className}"
@pointerdown=${(e: MouseEvent) => e.stopPropagation()}
@click=${() => {

View File

@@ -70,6 +70,7 @@ export class AISubItemList extends WithDisposable(LitElement) {
subItem => subItem.type,
subItem =>
html`<div
data-testid=${subItem.testId}
class="menu-item"
@click=${() => this._handleClick(subItem)}
>

View File

@@ -4,11 +4,13 @@ import type { TemplateResult } from 'lit';
export interface AIItemGroupConfig {
name?: string;
testId?: string;
items: AIItemConfig[];
}
export interface AIItemConfig {
name: string;
testId: string;
icon: TemplateResult | (() => HTMLElement);
showWhen?: (
chain: Chain<InitCommandCtx>,
@@ -23,6 +25,7 @@ export interface AIItemConfig {
export interface AISubItemConfig {
type: string;
testId?: string;
handler?: (host: EditorHost) => void;
}

View File

@@ -131,6 +131,7 @@ export class AskAIButton extends WithDisposable(LitElement) {
});
return html`<div
class="ask-ai-button"
data-testid="ask-ai-button"
style=${buttonStyles}
${toggleType === 'hover' ? ref(this._whenHover.setReference) : nothing}
@click=${this._toggleAIPanel}

View File

@@ -110,7 +110,11 @@ export class AskAIToolbarButton extends WithDisposable(LitElement) {
};
override render() {
return html`<div class="ask-ai-button" @click=${this._onClick}>
return html`<div
class="ask-ai-button"
data-testid="ask-ai-button"
@click=${this._onClick}
>
<ask-ai-icon .size=${'middle'}></ask-ai-icon>
</div>`;
}

View File

@@ -92,6 +92,9 @@ export class ChatActionList extends LitElement {
@property({ attribute: false })
accessor withMargin = false;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-action-list';
override render() {
const { actions } = this;
if (!actions.length) {

View File

@@ -127,6 +127,9 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor retry = () => {};
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-actions';
private _toggle() {
this._morePopper?.toggle();
}
@@ -197,7 +200,11 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
: nothing}
${isLast
? nothing
: html`<div class="button more" @click=${this._toggle}>
: html`<div
class="button more"
data-testid="action-more-button"
@click=${this._toggle}
>
${MoreHorizontalIcon({ width: '20px', height: '20px' })}
</div> `}
</div>

View File

@@ -84,6 +84,7 @@ export type TextRendererOptions = {
customHeading?: boolean;
extensions?: ExtensionType[];
additionalMiddlewares?: TransformerMiddleware[];
testId?: string;
};
export const CustomPageEditorBlockSpecs: ExtensionType[] = [
@@ -290,13 +291,13 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
return nothing;
}
const { customHeading } = this.options;
const { customHeading, testId } = this.options;
const classes = classMap({
'text-renderer-container': true,
'custom-heading': !!customHeading,
});
return html`
<div class=${classes}>
<div class=${classes} data-testid=${testId}>
${keyed(
this._doc,
html`<div class="ai-answer-text-editor affine-page-viewport">

View File

@@ -61,6 +61,7 @@ import {
const translateSubItem = translateLangs.map(lang => {
return {
type: lang,
testId: `action-translate-${lang}`,
handler: actionToHandler('translate', AIStarIconWithAnimation, { lang }),
};
});
@@ -68,6 +69,7 @@ const translateSubItem = translateLangs.map(lang => {
const toneSubItem = textTones.map(tone => {
return {
type: tone,
testId: `action-change-tone-${tone.toLowerCase()}`,
handler: actionToHandler('changeTone', AIStarIconWithAnimation, { tone }),
};
});
@@ -75,6 +77,7 @@ const toneSubItem = textTones.map(tone => {
export const imageFilterSubItem = imageFilterStyles.map(style => {
return {
type: style,
testId: `action-image-filter-${style.toLowerCase().replace(' ', '-')}`,
handler: actionToHandler(
'filterImage',
AIImageIconWithAnimation,
@@ -89,6 +92,7 @@ export const imageFilterSubItem = imageFilterStyles.map(style => {
export const imageProcessingSubItem = imageProcessingTypes.map(type => {
return {
type,
testId: `action-image-processing-${type.toLowerCase().replace(' ', '-')}`,
handler: actionToHandler(
'processImage',
AIImageIconWithAnimation,
@@ -105,6 +109,7 @@ const othersGroup: AIItemGroupConfig = {
items: [
{
name: 'Continue with AI',
testId: 'action-continue-with-ai',
icon: CommentIcon({ width: '20px', height: '20px' }),
showWhen: () => true,
handler: host => {
@@ -125,18 +130,21 @@ const editGroup: AIItemGroupConfig = {
items: [
{
name: 'Translate to',
testId: 'action-translate',
icon: LanguageIcon(),
showWhen: noteBlockOrTextShowWhen,
subItem: translateSubItem,
},
{
name: 'Change tone to',
testId: 'action-change-tone',
icon: ToneIcon(),
showWhen: noteBlockOrTextShowWhen,
subItem: toneSubItem,
},
{
name: 'Improve writing',
testId: 'action-improve-writing',
icon: ImproveWritingIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('improveWriting', AIStarIconWithAnimation),
@@ -144,18 +152,21 @@ const editGroup: AIItemGroupConfig = {
{
name: 'Make it longer',
testId: 'action-make-it-longer',
icon: LongerIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('makeLonger', AIStarIconWithAnimation),
},
{
name: 'Make it shorter',
testId: 'action-make-it-shorter',
icon: ShorterIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('makeShorter', AIStarIconWithAnimation),
},
{
name: 'Continue writing',
testId: 'action-continue-writing',
icon: PenIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('continueWriting', AIPenIconWithAnimation),
@@ -168,30 +179,35 @@ const draftGroup: AIItemGroupConfig = {
items: [
{
name: 'Write an article about this',
testId: 'action-write-article',
icon: PenIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('writeArticle', AIPenIconWithAnimation),
},
{
name: 'Write a tweet about this',
testId: 'action-write-twitter-post',
icon: PenIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('writeTwitterPost', AIPenIconWithAnimation),
},
{
name: 'Write a poem about this',
testId: 'action-write-poem',
icon: PenIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('writePoem', AIPenIconWithAnimation),
},
{
name: 'Write a blog post about this',
testId: 'action-write-blog-post',
icon: PenIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('writeBlogPost', AIPenIconWithAnimation),
},
{
name: 'Brainstorm ideas about this',
testId: 'action-brainstorm',
icon: PenIcon(),
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('brainstorm', AIPenIconWithAnimation),
@@ -205,18 +221,21 @@ const reviewGroup: AIItemGroupConfig = {
{
name: 'Fix spelling',
icon: PenIcon(),
testId: 'action-fix-spelling',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('fixSpelling', AIStarIconWithAnimation),
},
{
name: 'Fix grammar',
icon: PenIcon(),
testId: 'action-fix-grammar',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('improveGrammar', AIStarIconWithAnimation),
},
{
name: 'Explain this image',
icon: PenIcon(),
testId: 'action-explain-image',
showWhen: imageOnlyShowWhen,
handler: actionToHandler(
'explainImage',
@@ -228,18 +247,21 @@ const reviewGroup: AIItemGroupConfig = {
{
name: 'Explain this code',
icon: ExplainIcon(),
testId: 'action-explain-code',
showWhen: noteWithCodeBlockShowWen,
handler: actionToHandler('explainCode', AIStarIconWithAnimation),
},
{
name: 'Check code error',
icon: ExplainIcon(),
testId: 'action-check-code-error',
showWhen: noteWithCodeBlockShowWen,
handler: actionToHandler('checkCodeErrors', AIStarIconWithAnimation),
},
{
name: 'Explain selection',
icon: SelectionIcon({ width: '20px', height: '20px' }),
testId: 'action-explain-selection',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('explain', AIStarIconWithAnimation),
},
@@ -252,19 +274,22 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'Summarize',
icon: PenIcon(),
testId: 'action-summarize',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('summary', AIPenIconWithAnimation),
},
{
name: 'Generate headings',
icon: PenIcon(),
handler: actionToHandler('createHeadings', AIPenIconWithAnimation),
testId: 'action-generate-headings',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('createHeadings', AIPenIconWithAnimation),
beta: true,
},
{
name: 'Generate an image',
icon: ImageIcon(),
testId: 'action-generate-image',
showWhen: notAllAIChatBlockShowWhen,
handler: actionToHandler(
'createImage',
@@ -339,12 +364,14 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'Generate outline',
icon: PenIcon(),
testId: 'action-generate-outline',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('writeOutline', AIPenIconWithAnimation),
},
{
name: 'Expand from this mind map node',
icon: MindmapNodeIcon(),
testId: 'action-expand-mindmap-node',
showWhen: mindmapChildShowWhen,
handler: actionToHandler(
'expandMindmap',
@@ -370,12 +397,14 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'Brainstorm ideas with mind map',
icon: MindmapIcon(),
testId: 'action-brainstorm-mindmap',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('brainstormMindmap', AIMindMapIconWithAnimation),
},
{
name: 'Regenerate mind map',
icon: MindmapIcon(),
testId: 'action-regenerate-mindmap',
showWhen: mindmapRootShowWhen,
handler: actionToHandler(
'brainstormMindmap',
@@ -388,6 +417,7 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'Generate presentation',
icon: PresentationIcon(),
testId: 'action-generate-presentation',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('createSlides', AIPresentationIconWithAnimation),
beta: true,
@@ -395,6 +425,7 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'Make it real',
icon: MakeItRealIcon({ width: '20px', height: '20px' }),
testId: 'action-make-it-real',
beta: true,
showWhen: notAllAIChatBlockShowWhen,
handler: actionToHandler(
@@ -476,6 +507,7 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'AI image filter',
icon: PenIcon(),
testId: 'action-ai-image-filter',
showWhen: imageOnlyShowWhen,
subItem: imageFilterSubItem,
subItemOffset: [12, -4],
@@ -484,6 +516,7 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'Image processing',
icon: ImageIcon(),
testId: 'action-image-processing',
showWhen: imageOnlyShowWhen,
subItem: imageProcessingSubItem,
subItemOffset: [12, -6],
@@ -492,6 +525,7 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'Generate a caption',
icon: PenIcon(),
testId: 'action-generate-caption',
showWhen: imageOnlyShowWhen,
beta: true,
handler: actionToHandler(
@@ -504,6 +538,7 @@ const generateGroup: AIItemGroupConfig = {
{
name: 'Find actions',
icon: SearchIcon(),
testId: 'action-find-actions',
showWhen: noteBlockOrTextShowWhen,
handler: actionToHandler('findActions', AIStarIconWithAnimation),
beta: true,

View File

@@ -181,6 +181,9 @@ export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) {
@property({ attribute: false })
accessor showDetailPanel: boolean = false;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'ai-error';
}
const PaymentRequiredErrorRenderer = (host: EditorHost) => html`

View File

@@ -107,7 +107,7 @@ export const createImageRenderer: (
object-fit: contain;
}
</style>
<div class="ai-answer-image">
<div class="ai-answer-image" data-testid="ai-answer-image">
<img src=${answer}></img>
</div>`;

View File

@@ -188,12 +188,21 @@ export class ChatBlockInput extends SignalWatcher(LitElement) {
`
: nothing}
${images.length < MaximumImageCount
? html`<div class="image-upload" @click=${this._handleImageUpload}>
? html`<div
data-testid="chat-block-input-image-upload"
class="image-upload"
@click=${this._handleImageUpload}
>
${ImageIcon()}
</div>`
: nothing}
${status === 'transmitting'
? html`<div @click=${this._handleAbort}>${ChatAbortIcon}</div>`
? html`<div
@click=${this._handleAbort}
data-testid="chat-panel-peek-view-stop"
>
${ChatAbortIcon}
</div>`
: html`<div
@click=${this._onTextareaSend}
class="chat-panel-send"

View File

@@ -517,7 +517,12 @@ export class AffineAIPanelWidget extends WidgetComponent {
],
]);
return html`<div class="ai-panel-container">${mainTemplate}</div>`;
return html`<div
class="ai-panel-container"
data-testid="ai-panel-container"
>
${mainTemplate}
</div>`;
}
protected override willUpdate(changed: PropertyValues): void {

View File

@@ -74,9 +74,12 @@ export class AIFinishTip extends WithDisposable(LitElement) {
${this.copy?.allowed
? html`<div class="right">
${this.copied
? html`<div class="copied">${AIDoneIcon}</div>`
? html`<div class="copied" data-testid="answer-copied">
${AIDoneIcon}
</div>`
: html`<div
class="copy"
data-testid="answer-copy-button"
@click=${async () => {
this.copied = !!(await this.copy?.onCopy());
if (this.copied) {

View File

@@ -86,7 +86,7 @@ export class AIPanelAnswer extends WithDisposable(LitElement) {
return html`
<div class="answer">
<div class="answer-head">Answer</div>
<div class="answer-body">
<div class="answer-body" data-testid="answer-content">
<slot></slot>
</div>
</div>
@@ -104,7 +104,10 @@ export class AIPanelAnswer extends WithDisposable(LitElement) {
${index !== 0
? html`<ai-panel-divider></ai-panel-divider>`
: nothing}
<div class="response-list-container">
<div
class="response-list-container"
data-testid=${group.testId}
>
<ai-item-list
.host=${this.host}
.groups=${[group]}
@@ -143,6 +146,9 @@ export class AIPanelAnswer extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'ai-penel-answer';
}
declare global {

View File

@@ -210,7 +210,7 @@ export class AIPanelError extends WithDisposable(LitElement) {
);
return html`
<div class="error">
<div class="error" data-testid="ai-error">
<div class="answer-tip">
<div class="answer-label">Answer</div>
<slot></slot>

View File

@@ -85,10 +85,10 @@ export class AIPanelGenerating extends WithDisposable(LitElement) {
.showHeader=${!this.withAnswer}
></generating-placeholder>`
: nothing}
<div class="generating-tip">
<div class="generating-tip" data-testid="ai-generating">
<div class="left">${generatingIcon}</div>
<div class="text">AI is generating...</div>
<div @click=${this.stopGenerating} class="right">
<div @click=${this.stopGenerating} class="right" data-testid="ai-stop">
<span class="stop-icon">${AIStopIcon}</span>
<span class="esc-label">ESC</span>
</div>

View File

@@ -62,6 +62,7 @@ export class EdgelessCopilotToolbarEntry extends WithDisposable(LitElement) {
return html`<edgeless-tool-icon-button
aria-label="Ask AI"
class="copilot-icon-button"
data-testid="ask-ai-button"
@click=${this._onClick}
>
${AIStarIcon} <span class="label medium">Ask AI</span>