mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
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:
@@ -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(
|
||||
|
||||
@@ -462,7 +462,6 @@ export function noteBlockOrTextShowWhen(
|
||||
host: EditorHost
|
||||
) {
|
||||
const selected = getCopilotSelectedElems(host);
|
||||
|
||||
return selected.some(
|
||||
el =>
|
||||
el instanceof NoteBlockModel ||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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=${() => {
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+8
-2
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# AFFiNE Cloud Copilot E2E Tests
|
||||
|
||||
This directory contains end-to-end tests for the AFFiNE Cloud Copilot feature. The tests are organized in a structured way to ensure comprehensive coverage of different functionalities.
|
||||
|
||||
## Test 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.
|
||||
|
||||
## Test Utilities
|
||||
|
||||
The `/utils` directory contains shared utilities for testing:
|
||||
|
||||
- **ChatPanelUtils**: Helper functions for chat panel interactions
|
||||
- **EditorUtils**: Helper functions for editor operations
|
||||
- **TestUtils**: General test utilities and setup functions
|
||||
@@ -0,0 +1,56 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/BrainstormIdeasWithMindMap', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate a mind map for the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { brainstormMindMap } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'Panda'
|
||||
);
|
||||
const { answer, responses } = await brainstormMindMap();
|
||||
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate a mind map for the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { brainstormMindMap } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(page, 'Panda');
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await brainstormMindMap();
|
||||
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate a mind map for the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { brainstormMindMap } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(page, 'Panda');
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await brainstormMindMap();
|
||||
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/ChangeTone', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support changing the tone of the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { changeTone } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a great note-taking app'
|
||||
);
|
||||
const { answer, responses } = await changeTone('informal');
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support changing the tone of the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { changeTone } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a great note-taking app'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await changeTone('informal');
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support changing the tone of the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { changeTone } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a great note-taking app'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await changeTone('informal');
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { changeTone } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a great note-taking app'
|
||||
);
|
||||
const { answer } = await changeTone('informal');
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Change tone/);
|
||||
await expect(actionName).toHaveText(/Change tone/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/CheckCodeError', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support check code error', async ({ page, utils }) => {
|
||||
const { checkCodeError } = await utils.editor.askAIWithCode(
|
||||
page,
|
||||
'consloe.log("Hello,World!");',
|
||||
'javascript'
|
||||
);
|
||||
const { answer, responses } = await checkCodeError();
|
||||
await expect(answer).toHaveText(/console/);
|
||||
await expect(responses).toEqual(
|
||||
new Set(['insert-below', 'replace-selection'])
|
||||
);
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { checkCodeError } = await utils.editor.askAIWithCode(
|
||||
page,
|
||||
'consloe.log("Hello,World!");',
|
||||
'javascript'
|
||||
);
|
||||
const { answer } = await checkCodeError();
|
||||
const insert = answer.getByTestId('answer-insert-below');
|
||||
await insert.click();
|
||||
await utils.chatPanel.waitForHistory(page, [{ role: 'action' }]);
|
||||
const {
|
||||
message,
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(
|
||||
message.getByTestId('original-text').locator('affine-code')
|
||||
).toBeVisible();
|
||||
await expect(panelAnswer).toHaveText(/console/);
|
||||
await expect(prompt).toHaveText(/Check the code error of the follow code/);
|
||||
await expect(actionName).toHaveText(/Check code error/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/ContinueWithAI', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
});
|
||||
|
||||
test('should support continue in chat panel', async ({ page, utils }) => {
|
||||
const { continueWithAi } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
await continueWithAi();
|
||||
const chatPanelInput = await page.getByTestId('chat-panel-input-container');
|
||||
const quote = await chatPanelInput.getByTestId('chat-selection-quote');
|
||||
await expect(quote).toHaveText(/Apple/, { timeout: 10000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/ContinueWriting', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support continue writing the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1280, height: 2000 });
|
||||
const { continueWriting } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await continueWriting();
|
||||
await expect(answer).toHaveText(/,*/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support continue writing the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { continueWriting } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await continueWriting();
|
||||
await expect(answer).toHaveText(/,*/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support continue writing the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { continueWriting } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await continueWriting();
|
||||
await expect(answer).toHaveText(/,*/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { continueWriting } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await continueWriting();
|
||||
const insert = answer.getByTestId('answer-insert-below');
|
||||
await insert.click();
|
||||
await utils.chatPanel.waitForHistory(
|
||||
page,
|
||||
[
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
],
|
||||
10000
|
||||
);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/,*/);
|
||||
await expect(prompt).toHaveText(/Continue the following text/);
|
||||
await expect(actionName).toHaveText(/Continue writing/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('expand mindmap node', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should expand the mindmap node', async ({ page, utils }) => {
|
||||
let id: string;
|
||||
const { expandMindMapNode } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
id = await utils.editor.createMindmap(page);
|
||||
},
|
||||
async () => {
|
||||
// Select the first child in the mindmap
|
||||
const { id: childId } = await utils.editor.getMindMapNode(
|
||||
page,
|
||||
id!,
|
||||
[0, 0]
|
||||
);
|
||||
await utils.editor.selectElementInEdgeless(page, [childId]);
|
||||
}
|
||||
);
|
||||
await expandMindMapNode();
|
||||
// Child node should be expanded
|
||||
await expect(async () => {
|
||||
const newChild = await utils.editor.getMindMapNode(page, id!, [0, 0, 0]);
|
||||
expect(newChild).toBeDefined();
|
||||
}).toPass({ timeout: 20000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/ExplainCode', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support explain code', async ({ page, utils }) => {
|
||||
const { explainCode } = await utils.editor.askAIWithCode(
|
||||
page,
|
||||
'console.log("Hello, World!");',
|
||||
'javascript'
|
||||
);
|
||||
const { answer } = await explainCode();
|
||||
await expect(answer).toHaveText(/console.log/);
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { explainCode } = await utils.editor.askAIWithCode(
|
||||
page,
|
||||
'console.log("Hello, World!");',
|
||||
'javascript'
|
||||
);
|
||||
const { answer } = await explainCode();
|
||||
const insert = answer.getByTestId('answer-insert-below');
|
||||
await insert.click();
|
||||
await utils.chatPanel.waitForHistory(page, [{ role: 'action' }]);
|
||||
const {
|
||||
message,
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(
|
||||
message.getByTestId('original-text').locator('affine-code')
|
||||
).toBeVisible();
|
||||
await expect(panelAnswer).toHaveText(/console.log/);
|
||||
await expect(prompt).toHaveText(/Analyze and explain the follow code/);
|
||||
await expect(actionName).toHaveText(/Explain this code/);
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,78 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/ExplainSelection', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support explaining the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { explainSelection } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'LLM(AI)'
|
||||
);
|
||||
const { answer, responses } = await explainSelection();
|
||||
await expect(answer).toHaveText(/Large Language Model/, { timeout: 20000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support explaining the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { explainSelection } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(page, 'LLM(AI)');
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await explainSelection();
|
||||
await expect(answer).toHaveText(/Large Language Model/, { timeout: 20000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support explaining the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { explainSelection } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(page, 'LLM(AI)');
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await explainSelection();
|
||||
await expect(answer).toHaveText(/Large Language Model/, { timeout: 20000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { explainSelection } = await utils.editor.askAIWithText(page, 'LLM');
|
||||
const { answer } = await explainSelection();
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/Large Language Model/);
|
||||
await expect(prompt).toHaveText(/Analyze and explain the follow text/);
|
||||
await expect(actionName).toHaveText(/Explain this/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/FindActions', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should find actions for selected content', async ({ page, utils }) => {
|
||||
const { findActions } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
`Choose a Booking Platform
|
||||
Enter Travel Details
|
||||
Compare and Select Flights`
|
||||
);
|
||||
const { answer, responses } = await findActions();
|
||||
const todos = await answer.locator('affine-list').all();
|
||||
|
||||
const expectedTexts = [
|
||||
'Choose a Booking Platform',
|
||||
'Enter Travel Details',
|
||||
'Compare and Select Flights',
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
todos.map(async (todo, index) => {
|
||||
await expect(
|
||||
todo.locator('.affine-list-block__todo-prefix')
|
||||
).toBeVisible();
|
||||
await expect(todo).toHaveText(expectedTexts[index]);
|
||||
})
|
||||
);
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should find actions for selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { findActions } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'Choose a Booking Platform'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await findActions();
|
||||
const todos = await answer.locator('affine-list').all();
|
||||
const expectedTexts = [
|
||||
'Choose a Booking Platform',
|
||||
'Enter Travel Details',
|
||||
'Compare and Select Flights',
|
||||
];
|
||||
await Promise.all(
|
||||
todos.map(async (todo, index) => {
|
||||
await expect(
|
||||
todo.locator('.affine-list-block__todo-prefix')
|
||||
).toBeVisible();
|
||||
await expect(todo).toHaveText(expectedTexts[index]);
|
||||
})
|
||||
);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should find actions for selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { findActions } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'Choose a Booking Platform'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await findActions();
|
||||
const todos = await answer.locator('affine-list').all();
|
||||
const expectedTexts = [
|
||||
'Choose a Booking Platform',
|
||||
'Enter Travel Details',
|
||||
'Compare and Select Flights',
|
||||
];
|
||||
await Promise.all(
|
||||
todos.map(async (todo, index) => {
|
||||
await expect(
|
||||
todo.locator('.affine-list-block__todo-prefix')
|
||||
).toBeVisible();
|
||||
await expect(todo).toHaveText(expectedTexts[index]);
|
||||
})
|
||||
);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { findActions } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
`Choose a Booking Platform
|
||||
Enter Travel Details
|
||||
Compare and Select Flights`
|
||||
);
|
||||
const { answer } = await findActions();
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
const todos = await panelAnswer.locator('affine-list').all();
|
||||
|
||||
const expectedTexts = [
|
||||
'Choose a Booking Platform',
|
||||
'Enter Travel Details',
|
||||
'Compare and Select Flights',
|
||||
];
|
||||
await Promise.all(
|
||||
todos.map(async (todo, index) => {
|
||||
await expect(
|
||||
todo.locator('.affine-list-block__todo-prefix')
|
||||
).toBeVisible();
|
||||
await expect(todo).toHaveText(expectedTexts[index]);
|
||||
})
|
||||
);
|
||||
await expect(prompt).toHaveText(/Find action items of the follow text/);
|
||||
await expect(actionName).toHaveText(/Find action items from it/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/FixGrammar', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support fixing grammatical errors in the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { fixGrammar } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'I is a student'
|
||||
);
|
||||
const { answer, responses } = await fixGrammar();
|
||||
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support fixing grammatical errors in the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { fixGrammar } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(page, 'I is a student');
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await fixGrammar();
|
||||
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support fixing grammatical errors in the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { fixGrammar } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(page, 'I is a student');
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await fixGrammar();
|
||||
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { fixGrammar } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'I is a student'
|
||||
);
|
||||
const { answer } = await fixGrammar();
|
||||
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/I am a student/);
|
||||
await expect(prompt).toHaveText(
|
||||
/Improve the grammar of the following text/
|
||||
);
|
||||
await expect(actionName).toHaveText(/Improve grammar for it/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/FixSpelling', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support fixing spelling errors in the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { fixSpelling } = await utils.editor.askAIWithText(page, 'Appel');
|
||||
const { answer, responses } = await fixSpelling();
|
||||
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support fixing spelling errors in the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { fixSpelling } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(page, 'Appel');
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await fixSpelling();
|
||||
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support fixing spelling errors in the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { fixSpelling } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(page, 'Appel');
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await fixSpelling();
|
||||
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { fixSpelling } = await utils.editor.askAIWithText(page, 'Appel');
|
||||
const { answer } = await fixSpelling();
|
||||
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/Apple/);
|
||||
await expect(prompt).toHaveText(
|
||||
/Correct the spelling of the following text/
|
||||
);
|
||||
await expect(actionName).toHaveText(/Fix spelling for it/);
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,71 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/GenerateAnImageWithText', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate an image for the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generateImage } = await utils.editor.askAIWithText(page, 'Panda');
|
||||
const { answer, responses } = await generateImage();
|
||||
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate an image for the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generateImage } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(page, 'Panda');
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await generateImage();
|
||||
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate an image for the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generateImage } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(page, 'Panda');
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await generateImage();
|
||||
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { generateImage } = await utils.editor.askAIWithText(page, 'Panda');
|
||||
const { answer } = await generateImage();
|
||||
const insert = answer.getByTestId('answer-insert-below');
|
||||
await insert.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const { answer: panelAnswer, actionName } =
|
||||
await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(
|
||||
panelAnswer.getByTestId('generated-image').locator('img')
|
||||
).toBeVisible();
|
||||
await expect(actionName).toHaveText(/image/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/GenerateHeadings', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate headings for selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generateHeadings } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await generateHeadings();
|
||||
await Promise.race([
|
||||
answer.locator('h1').isVisible(),
|
||||
answer.locator('h2').isVisible(),
|
||||
answer.locator('h3').isVisible(),
|
||||
]);
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-above', 'replace-selection']));
|
||||
});
|
||||
|
||||
test.fixme(
|
||||
'should generate headings for selected text block in edgeless',
|
||||
async ({ page, utils }) => {
|
||||
const { generateHeadings } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await generateHeadings();
|
||||
await Promise.race([
|
||||
answer.locator('h1').isVisible(),
|
||||
answer.locator('h2').isVisible(),
|
||||
answer.locator('h3').isVisible(),
|
||||
]);
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-above']));
|
||||
}
|
||||
);
|
||||
|
||||
test.fixme(
|
||||
'should generate headings for selected note block in edgeless',
|
||||
async ({ page, utils }) => {
|
||||
const { generateHeadings } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await generateHeadings();
|
||||
await Promise.race([
|
||||
answer.locator('h1').isVisible(),
|
||||
answer.locator('h2').isVisible(),
|
||||
answer.locator('h3').isVisible(),
|
||||
]);
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-above']));
|
||||
}
|
||||
);
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { generateHeadings } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await generateHeadings();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await Promise.race([
|
||||
panelAnswer.locator('h1').isVisible(),
|
||||
panelAnswer.locator('h2').isVisible(),
|
||||
panelAnswer.locator('h3').isVisible(),
|
||||
]);
|
||||
await expect(prompt).toHaveText(/Create headings of the follow text/);
|
||||
await expect(actionName).toHaveText(/Create headings/);
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,86 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/GenerateOutline', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate outline for selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generateOutline } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await generateOutline();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should generate outline for selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generateOutline } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await generateOutline();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate outline for selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generateOutline } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await generateOutline();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { generateOutline } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await generateOutline();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Write an outline about this/);
|
||||
await expect(actionName).toHaveText(/Write outline/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/GeneratePresentation', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate a presentation for the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generatePresentation } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await generatePresentation();
|
||||
await expect(answer.locator('ai-slides-renderer')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate a presentation for the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generatePresentation } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await generatePresentation();
|
||||
await expect(answer.locator('ai-slides-renderer')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate a presentation for the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { generatePresentation } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await generatePresentation();
|
||||
await expect(answer.locator('ai-slides-renderer')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,80 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/ImproveWriting', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support improving the writing of the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { improveWriting } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is so smart'
|
||||
);
|
||||
const { answer, responses } = await improveWriting();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support improving the writing of the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { improveWriting } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(page, 'AFFiNE is so smart');
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await improveWriting();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support improving the writing of the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { improveWriting } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(page, 'AFFiNE is so smart');
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await improveWriting();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { improveWriting } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is so smart'
|
||||
);
|
||||
const { answer } = await improveWriting();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Improve the follow text/);
|
||||
await expect(actionName).toHaveText(/Improve writing for it/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/MakeItLonger', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support making the selected content longer', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItLonger } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await makeItLonger();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support making the selected text block longer in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItLonger } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await makeItLonger();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support making the selected note block longer in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItLonger } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await makeItLonger();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { makeItLonger } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await makeItLonger();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Expand the following text/);
|
||||
await expect(actionName).toHaveText(/Make it longer/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/MakeItReal', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support making the selected content to real', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItReal } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await makeItReal();
|
||||
await expect(answer.locator('iframe')).toBeVisible({ timeout: 30000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support making the selected text block to real in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItReal } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await makeItReal();
|
||||
await expect(answer.locator('iframe')).toBeVisible({ timeout: 30000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support making the selected note block to real in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItReal } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await makeItReal();
|
||||
await expect(answer.locator('iframe')).toBeVisible({ timeout: 30000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { makeItReal } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await makeItReal();
|
||||
const insert = answer.getByTestId('answer-insert-below');
|
||||
await insert.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer.locator('affine-code')).toBeVisible();
|
||||
await expect(prompt).toHaveText(/Write a web page of follow text/);
|
||||
await expect(actionName).toHaveText(/Make it real with text/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/MakeItShorter', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support making the selected content shorter', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItShorter } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await makeItShorter();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support making the selected text block shorter in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItShorter } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await makeItShorter();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support making the selected note block shorter in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { makeItShorter } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await makeItShorter();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { makeItShorter } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await makeItShorter();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Shorten the follow text/);
|
||||
await expect(actionName).toHaveText(/Make it shorter/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/RegenerateMindMap', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support regenerate the mind map for mindmap root', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
let id: string;
|
||||
const { regenerateMindMap } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
id = await utils.editor.createMindmap(page);
|
||||
},
|
||||
async () => {
|
||||
const { id: rootId } = await utils.editor.getMindMapNode(page, id!, [
|
||||
0,
|
||||
]);
|
||||
await utils.editor.selectElementInEdgeless(page, [rootId]);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, responses } = await regenerateMindMap();
|
||||
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
|
||||
expect(responses).toEqual(new Set(['replace-selection']));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/Summarize', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support summarizing the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { summarize } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await summarize();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support summarizing the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { summarize } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await summarize();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support summarizing the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { summarize } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await summarize();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { summarize } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await summarize();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Summary the follow text/);
|
||||
await expect(actionName).toHaveText(/Summary/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/Translate', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support translating the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer, responses } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support translating the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { translate } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(page, 'Apple');
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support translating the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { translate } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(page, 'Apple');
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('support show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/Apfel/);
|
||||
await expect(prompt).toHaveText(/Translate/);
|
||||
await expect(actionName).toHaveText(/Translate/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/WriteAnArticleAboutThis', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate an article for the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeArticle } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await writeArticle();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support writing an article for the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeArticle } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await writeArticle();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support writing an article for the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeArticle } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await writeArticle();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { writeArticle } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await writeArticle();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Write an article about this/);
|
||||
await expect(actionName).toHaveText(/Write an article about this/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/WriteAnBlogPostAboutThis', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate an blog post for the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeBlogPost } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await writeBlogPost();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should support writing an blog post for the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeBlogPost } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await writeBlogPost();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should support writing an blog post for the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeBlogPost } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await writeBlogPost();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { writeBlogPost } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await writeBlogPost();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Write a blog post about this/);
|
||||
await expect(actionName).toHaveText(/Write a blog post about this/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/WriteAnPoemAboutThis', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate an poem for the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writePoem } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await writePoem();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should generate an poem for the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writePoem } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await writePoem();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate an poem for the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writePoem } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await writePoem();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { writePoem } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await writePoem();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Write a poem about this/);
|
||||
await expect(actionName).toHaveText(/Write a poem about this/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIAction/WriteAnTweetAboutThis', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should generate an tweet for the selected content', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeTwitterPost } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer, responses } = await writeTwitterPost();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
|
||||
});
|
||||
|
||||
test('should generate an tweet for the selected text block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeTwitterPost } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await writeTwitterPost();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should generate an tweet for the selected note block in edgeless', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { writeTwitterPost } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
}
|
||||
);
|
||||
const { answer, responses } = await writeTwitterPost();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
expect(responses).toEqual(new Set(['insert-below']));
|
||||
});
|
||||
|
||||
test('should show chat history in chat panel', async ({ page, utils }) => {
|
||||
const { writeTwitterPost } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE is a workspace with fully merged docs'
|
||||
);
|
||||
const { answer } = await writeTwitterPost();
|
||||
await expect(answer).toHaveText(/AFFiNE/);
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'action',
|
||||
},
|
||||
]);
|
||||
const {
|
||||
answer: panelAnswer,
|
||||
prompt,
|
||||
actionName,
|
||||
} = await utils.chatPanel.getLatestAIActionMessage(page);
|
||||
await expect(panelAnswer).toHaveText(/AFFiNE/);
|
||||
await expect(prompt).toHaveText(/Write a twitter about this/);
|
||||
await expect(actionName).toHaveText(/Write a twitter about this/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// eslint-disable no-empty-pattern
|
||||
import { test as base } from '@affine-test/kit/playwright';
|
||||
|
||||
import { ChatPanelUtils } from '../utils/chat-panel-utils';
|
||||
import { EditorUtils } from '../utils/editor-utils';
|
||||
import { TestUtils } from '../utils/test-utils';
|
||||
|
||||
interface TestUtilsFixtures {
|
||||
utils: {
|
||||
testUtils: TestUtils;
|
||||
chatPanel: typeof ChatPanelUtils;
|
||||
editor: typeof EditorUtils;
|
||||
};
|
||||
}
|
||||
|
||||
export const test = base.extend<TestUtilsFixtures>({
|
||||
utils: async ({}, use) => {
|
||||
const testUtils = TestUtils.getInstance();
|
||||
await use({
|
||||
testUtils,
|
||||
chatPanel: ChatPanelUtils,
|
||||
editor: EditorUtils,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export type TestFixtures = typeof test;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIBasic/Authority', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should show error & login button when no login', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await expect(page.getByTestId('ai-error')).toBeVisible();
|
||||
await expect(page.getByTestId('ai-error-action-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should support login in error state', async ({ page, utils }) => {
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
const loginButton = page.getByTestId('ai-error-action-button');
|
||||
await loginButton.click();
|
||||
|
||||
await expect(page.getByTestId('auth-modal')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,346 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIBasic/Chat', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should display empty state when no messages', async ({ page }) => {
|
||||
// Verify empty state UI
|
||||
await expect(page.getByTestId('chat-panel-empty-state')).toBeVisible();
|
||||
await expect(page.getByTestId('ai-onboarding')).toBeVisible();
|
||||
});
|
||||
|
||||
test(`should send message and receive AI response:
|
||||
- send message
|
||||
- AI is loading
|
||||
- AI generating
|
||||
- AI success
|
||||
`, async ({ page, utils }) => {
|
||||
// Type and send a message
|
||||
await utils.chatPanel.makeChat(page, 'Introduce AFFiNE to me');
|
||||
|
||||
// AI is loading
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Introduce AFFiNE to me',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'loading',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(page.getByTestId('ai-loading')).toBeVisible();
|
||||
|
||||
// AI Generating
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Introduce AFFiNE to me',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'transmitting',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(page.getByTestId('ai-loading')).not.toBeVisible();
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Introduce AFFiNE to me',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should support stop generating', async ({ page, utils }) => {
|
||||
await utils.chatPanel.makeChat(page, 'Introduce AFFiNE to me');
|
||||
|
||||
// AI Generating
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Introduce AFFiNE to me',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'transmitting',
|
||||
},
|
||||
]);
|
||||
|
||||
await page.getByTestId('chat-panel-stop').click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Introduce AFFiNE to me',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should render ai actions inline if the answer is the last one in the list, otherwise, nest them under the "More" menu', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.chatPanel.makeChat(page, 'Hello, how can you help me?');
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello, how can you help me?',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(page.getByTestId('chat-action-list')).toBeVisible();
|
||||
await utils.chatPanel.makeChat(page, 'Nice to meet you');
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello, how can you help me?',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'idle',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Nice to meet you',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
const firstAnswer = await page
|
||||
.getByTestId('chat-message-assistant')
|
||||
.first();
|
||||
const more = firstAnswer.getByTestId('action-more-button');
|
||||
await more.click();
|
||||
await expect(firstAnswer.getByTestId('chat-actions')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show scroll indicator when there are many messages', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
// Set window height to 100px to ensure scroll indicator appears
|
||||
await page.setViewportSize({ width: 1280, height: 400 });
|
||||
|
||||
// Type and send a message
|
||||
await utils.chatPanel.makeChat(
|
||||
page,
|
||||
'Hello, write a poem about the moon with 50 words.'
|
||||
);
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello, write a poem about the moon with 50 words.',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for the answer to be completely rendered
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Scroll up to trigger scroll indicator
|
||||
const chatMessagesContainer = page.getByTestId(
|
||||
'chat-panel-messages-container'
|
||||
);
|
||||
await chatMessagesContainer.evaluate(el => {
|
||||
el.scrollTop = 0;
|
||||
});
|
||||
|
||||
const scrollDownIndicator = page.getByTestId(
|
||||
'chat-panel-scroll-down-indicator'
|
||||
);
|
||||
|
||||
// Verify scroll indicator appears
|
||||
await expect(scrollDownIndicator).toBeVisible();
|
||||
|
||||
// Click scroll indicator to scroll to bottom
|
||||
await scrollDownIndicator.click();
|
||||
|
||||
// Verify scroll indicator disappears
|
||||
await expect(scrollDownIndicator).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should show error when request failed', async ({ page, utils }) => {
|
||||
// Simulate network error by disconnecting
|
||||
await page.route('**/graphql', route => route.abort('failed'));
|
||||
|
||||
// Send a message that will fail
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await expect(page.getByTestId('ai-error')).toBeVisible();
|
||||
await expect(page.getByTestId('action-retry-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should support retrying failed messages', async ({ page, utils }) => {
|
||||
// Simulate network error by disconnecting
|
||||
await page.route('**/graphql', route => route.abort('failed'));
|
||||
|
||||
// Send a message that will fail
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
// Verify error state
|
||||
await expect(page.getByTestId('ai-error')).toBeVisible();
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'error',
|
||||
},
|
||||
]);
|
||||
|
||||
// Reconnect network
|
||||
await page.route('**/graphql', route => route.continue());
|
||||
|
||||
await page.getByTestId('action-retry-button').click();
|
||||
|
||||
// Verify message is resent and AI responds
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should support retrying question', async ({ page, utils }) => {
|
||||
await utils.chatPanel.makeChat(
|
||||
page,
|
||||
'Introduce Large Language Model in under 500 words'
|
||||
);
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Introduce Large Language Model in under 500 words',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await page.pause();
|
||||
await actions.retry();
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Introduce Large Language Model in under 500 words',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'transmitting',
|
||||
},
|
||||
]);
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Introduce Large Language Model in under 500 words',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should support sending message with button', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.typeChat(page, 'Hello');
|
||||
await page.getByTestId('chat-panel-send').click();
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'loading',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should support clearing chat', async ({ page, utils }) => {
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
await utils.chatPanel.clearChat(page);
|
||||
await utils.chatPanel.waitForHistory(page, []);
|
||||
});
|
||||
|
||||
test('should support copying answer', async ({ page, utils }) => {
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.copy();
|
||||
await page.getByText('Copied to clipboard').isVisible();
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
const clipboardText = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
expect(clipboardText).toBe(content);
|
||||
}).toPass({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test.describe('AIBasic/Onboarding', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should show AI onboarding', async ({ page }) => {
|
||||
await expect(page.getByTestId('ai-onboarding')).toBeVisible();
|
||||
|
||||
// Show options
|
||||
await expect(
|
||||
page.getByTestId('read-foreign-language-article-with-ai')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('tidy-an-article-with-ai-mindmap-action')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('add-illustrations-to-the-article')
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('complete-writing-with-ai')).toBeVisible();
|
||||
await expect(page.getByTestId('freely-communicate-with-ai')).toBeVisible();
|
||||
});
|
||||
|
||||
test('read a foreign language article with AI', async ({ page, utils }) => {
|
||||
await page.getByTestId('read-foreign-language-article-with-ai').click();
|
||||
|
||||
await utils.editor.isEdgelessMode(page);
|
||||
const docTitle = await utils.editor.getDocTitle(page);
|
||||
await expect(docTitle).toContain('Read a foreign language');
|
||||
});
|
||||
|
||||
test('tidy an article with AI MindMap Action', async ({ page, utils }) => {
|
||||
await page.getByTestId('tidy-an-article-with-ai-mindmap-action').click();
|
||||
|
||||
await utils.editor.isEdgelessMode(page);
|
||||
const docTitle = await utils.editor.getDocTitle(page);
|
||||
await expect(docTitle).toContain('Tidy');
|
||||
});
|
||||
|
||||
test('add illustrations to the article', async ({ page, utils }) => {
|
||||
await page.getByTestId('add-illustrations-to-the-article').click();
|
||||
|
||||
await utils.editor.isEdgelessMode(page);
|
||||
const docTitle = await utils.editor.getDocTitle(page);
|
||||
await expect(docTitle).toContain('Add illustrations');
|
||||
});
|
||||
|
||||
test('complete writing with AI', async ({ page, utils }) => {
|
||||
await page.getByTestId('complete-writing-with-ai').click();
|
||||
|
||||
await utils.editor.isEdgelessMode(page);
|
||||
const docTitle = await utils.editor.getDocTitle(page);
|
||||
await expect(docTitle).toContain('Complete writing');
|
||||
});
|
||||
|
||||
test('freely communicate with AI', async ({ page, utils }) => {
|
||||
await page.getByTestId('freely-communicate-with-ai').click();
|
||||
|
||||
await utils.editor.isEdgelessMode(page);
|
||||
const docTitle = await utils.editor.getDocTitle(page);
|
||||
await expect(docTitle).toContain('Freely communicate');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/Attachments', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('support chat with attachment', async ({ page, utils }) => {
|
||||
const textContent = 'EEee is a cute cat';
|
||||
const buffer = Buffer.from(textContent);
|
||||
|
||||
await utils.chatPanel.chatWithAttachments(
|
||||
page,
|
||||
[
|
||||
{
|
||||
name: 'test.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: buffer,
|
||||
},
|
||||
],
|
||||
'What is EEee?'
|
||||
);
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is EEee?',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
expect(content).toMatch(/EEee/);
|
||||
}).toPass({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('support chat with multiple attachments', async ({ page, utils }) => {
|
||||
const textContent1 = 'EEee is a cute cat';
|
||||
const textContent2 = 'FFff is a cute dog';
|
||||
const buffer1 = Buffer.from(textContent1);
|
||||
const buffer2 = Buffer.from(textContent2);
|
||||
|
||||
await utils.chatPanel.chatWithAttachments(
|
||||
page,
|
||||
[
|
||||
{
|
||||
name: 'document1.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: buffer1,
|
||||
},
|
||||
{
|
||||
name: 'document2.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: buffer2,
|
||||
},
|
||||
],
|
||||
'What is EEee? What is FFff?'
|
||||
);
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is EEee? What is FFff?',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(async () => {
|
||||
const { content, message } =
|
||||
await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
expect(content).toMatch(/EEee/);
|
||||
expect(content).toMatch(/FFff/);
|
||||
expect(await message.locator('affine-footnote-node').count()).toBe(2);
|
||||
}).toPass({ timeout: 20000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/Collections', () => {});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { focusDocTitle } from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/Doc', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
expect(content).toMatch(/EEee/);
|
||||
}).toPass({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('support chat with specified docs', async ({ page, utils }) => {
|
||||
// Initialize the doc 1
|
||||
await focusDocTitle(page);
|
||||
await page.keyboard.insertText('Test Doc1');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.insertText('EEee is a cute cat');
|
||||
|
||||
// Initialize the doc 2
|
||||
await clickNewPageButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
await focusDocTitle(page);
|
||||
await page.keyboard.insertText('Test Doc2');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.insertText('FFff is a cute dog');
|
||||
|
||||
await utils.chatPanel.chatWithDoc(page, 'Test Doc1');
|
||||
await utils.chatPanel.chatWithDoc(page, 'Test Doc2');
|
||||
|
||||
await utils.chatPanel.makeChat(page, 'What is EEee? What is FFff?');
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is EEee? What is FFff?',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
expect(content).toMatch(/EEee/);
|
||||
expect(content).toMatch(/FFff/);
|
||||
}).toPass({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import type { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/EdgelessMindMap', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support replace mindmap with the regenerated one', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
let id: string;
|
||||
const { regenerateMindMap } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
id = await utils.editor.createMindmap(page);
|
||||
},
|
||||
async () => {
|
||||
const { id: rootId } = await utils.editor.getMindMapNode(page, id!, [
|
||||
0,
|
||||
]);
|
||||
await utils.editor.selectElementInEdgeless(page, [rootId]);
|
||||
}
|
||||
);
|
||||
|
||||
const { answer } = await regenerateMindMap();
|
||||
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
|
||||
// Expect original mindmap to be replaced
|
||||
const mindmaps = await page.evaluate(() => {
|
||||
const edgelessBlock = document.querySelector(
|
||||
'affine-edgeless-root'
|
||||
) as EdgelessRootBlockComponent;
|
||||
const mindmaps = edgelessBlock?.gfx.gfxElements
|
||||
.filter((el: GfxModel) => 'type' in el && el.type === 'mindmap')
|
||||
.map((el: GfxModel) => el.id);
|
||||
return mindmaps;
|
||||
});
|
||||
expect(mindmaps).toHaveLength(1);
|
||||
expect(mindmaps?.[0]).not.toBe(id!);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/EdgelessNoteBlock', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support insert a new note block below the current', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { translate } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessNote(page, 'Apple');
|
||||
}
|
||||
);
|
||||
const { answer } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
const insertBelow = answer.getByTestId('answer-insert-below');
|
||||
await insertBelow.click();
|
||||
await expect(page.locator('affine-edgeless-note').nth(1)).toHaveText(
|
||||
/Apfel/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/EdgelessShape', () => {});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/EdgelessTextBlock', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support insert answer below the current text', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
const { translate } = await utils.editor.askAIWithEdgeless(
|
||||
page,
|
||||
async () => {
|
||||
await utils.editor.createEdgelessText(page, 'Apple');
|
||||
}
|
||||
);
|
||||
const { answer } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
const insertBelow = answer.getByTestId('answer-insert-below');
|
||||
await insertBelow.click();
|
||||
await expect(page.locator('affine-edgeless-text')).toHaveText(
|
||||
/Apple[\s\S]*Apfel/
|
||||
);
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/tags', () => {});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIChatWith/Text', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should support stop generating', async ({ page, utils }) => {
|
||||
await utils.editor.askAIWithText(page, 'Appel');
|
||||
await page.getByTestId('action-fix-grammar').click();
|
||||
await expect(page.getByTestId('ai-generating')).toBeVisible();
|
||||
const stop = await page.getByTestId('ai-stop');
|
||||
await stop.click();
|
||||
await expect(page.getByTestId('ai-generating')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should support copy answer', async ({ page, utils }) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
const copy = answer.getByTestId('answer-copy-button');
|
||||
await copy.click();
|
||||
await expect(answer.getByTestId('answer-copied')).toBeVisible();
|
||||
const clipboardText = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
expect(clipboardText).toBe('Apfel');
|
||||
});
|
||||
|
||||
test('should support insert below', async ({ page, utils }) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
const insertBelow = answer.getByTestId('answer-insert-below');
|
||||
await insertBelow.click();
|
||||
const content = await utils.editor.getEditorContent(page);
|
||||
expect(content).toBe('Apple\nApfel');
|
||||
});
|
||||
|
||||
test('should support insert above', async ({ page, utils }) => {
|
||||
const { generateHeadings } = await utils.editor.askAIWithText(
|
||||
page,
|
||||
'AFFiNE'
|
||||
);
|
||||
const { answer } = await generateHeadings();
|
||||
await answer.locator('h1').isVisible();
|
||||
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
|
||||
|
||||
const insertAbove = answer.getByTestId('answer-insert-above');
|
||||
await insertAbove.click();
|
||||
const content = await utils.editor.getEditorContent(page);
|
||||
expect(content).toBe('AFFiNE\nAFFiNE');
|
||||
});
|
||||
|
||||
test('should support replace selection', async ({ page, utils }) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
const replace = answer.getByTestId('answer-replace');
|
||||
await replace.click();
|
||||
const content = await utils.editor.getEditorContent(page);
|
||||
expect(content).toBe('Apfel');
|
||||
});
|
||||
|
||||
test('should support continue in chat', async ({ page, utils }) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer } = await translate('German');
|
||||
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
|
||||
const continueInChat = answer.getByTestId('answer-continue-in-chat');
|
||||
await continueInChat.click();
|
||||
const chatPanelInput = await page.getByTestId('chat-panel-input-container');
|
||||
const quote = await chatPanelInput.getByTestId('chat-selection-quote');
|
||||
await expect(quote).toHaveText(/Apple/, { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should support regenerate', async ({ page, utils }) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer } = await translate('German');
|
||||
const regenerate = answer.getByTestId('answer-regenerate');
|
||||
await regenerate.click();
|
||||
const content = await utils.editor.getEditorContent(page);
|
||||
expect(content).toBe('Apple');
|
||||
});
|
||||
|
||||
test('should show error when request failed', async ({ page, utils }) => {
|
||||
await page.route('**/graphql', route => route.abort('failed'));
|
||||
await utils.editor.askAIWithText(page, 'Appel');
|
||||
await page.getByTestId('action-fix-spelling').click();
|
||||
await expect(page.getByTestId('ai-error')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should support retry when error', async ({ page, utils }) => {
|
||||
await page.route('**/graphql', route => route.abort('failed'));
|
||||
await utils.editor.askAIWithText(page, 'Appel');
|
||||
await page.getByTestId('action-fix-spelling').click();
|
||||
const aiPanelContainer = await page.getByTestId('ai-panel-container');
|
||||
|
||||
await page.route('**/graphql', route => route.continue());
|
||||
await aiPanelContainer.getByTestId('error-retry').click();
|
||||
const answer = await utils.editor.waitForAiAnswer(page);
|
||||
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should support discard', async ({ page, utils }) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer } = await translate('German');
|
||||
const discard = answer.getByTestId('answer-discard');
|
||||
await discard.click();
|
||||
await expect(answer).not.toBeVisible();
|
||||
const content = await utils.editor.getEditorContent(page);
|
||||
expect(content).toBe('Apple');
|
||||
});
|
||||
|
||||
test('should support discard when click outside', async ({ page, utils }) => {
|
||||
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
|
||||
const { answer } = await translate('German');
|
||||
await page.mouse.click(0, 0);
|
||||
await expect(page.getByText('Discard the AI result')).toBeVisible();
|
||||
await page.getByTestId('confirm-modal-confirm').click();
|
||||
await expect(answer).not.toBeVisible();
|
||||
const content = await utils.editor.getEditorContent(page);
|
||||
expect(content).toBe('Apple');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIInsertion/AddToEdgelessAsNote', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should only show option in edgeless mode', async ({ page, utils }) => {
|
||||
await utils.editor.focusToEditor(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
page.getByTestId('action-add-to-edgeless-as-note')
|
||||
).not.toBeVisible();
|
||||
|
||||
await utils.editor.switchToEdgelessMode(page);
|
||||
await expect(
|
||||
page.getByTestId('action-add-to-edgeless-as-note')
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should add to edgeless as note in edgeless mode', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.editor.switchToEdgelessMode(page);
|
||||
|
||||
// Delete default note
|
||||
await (await page.waitForSelector('affine-edgeless-note')).click();
|
||||
page.keyboard.press('Delete');
|
||||
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.addAsNote();
|
||||
await page.getByText('New note created');
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
const noteContent = await utils.editor.getNoteContent(page);
|
||||
expect(noteContent).toBe(content);
|
||||
}).toPass({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { focusDocTitle } from '@affine-test/kit/utils/editor';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIInsertion/Insert', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should insert content below selected block in page mode', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
// Create tow blocks
|
||||
// - Hello Block
|
||||
// - World Block
|
||||
await utils.editor.focusToEditor(page);
|
||||
await page.keyboard.insertText('Hello Block');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.insertText('World Block');
|
||||
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
// Focus to Hello
|
||||
// - Hello<cursor />
|
||||
await page.getByText('Hello Block').click();
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.insert();
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
const editorContent = await utils.editor.getEditorContent(page);
|
||||
expect(editorContent).toBe(`Hello Block\n${content}\nWorld Block`);
|
||||
}).toPass({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should insert content below selected block in edgeless mode', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.editor.switchToEdgelessMode(page);
|
||||
await utils.editor.focusToEditor(page);
|
||||
await page.keyboard.insertText('Hello Block');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.insertText('World Block');
|
||||
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
// Focus to Hello
|
||||
// - Hello<cursor />
|
||||
await page.locator('affine-edgeless-note').dblclick();
|
||||
await page.getByText('Hello Block').click();
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.insert();
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
const noteContent = await utils.editor.getNoteContent(page);
|
||||
expect(noteContent).toBe(`Hello Block\n${content}\nWorld Block`);
|
||||
}).toPass({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should insert content at the end of the page when no block is selected', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
// Create tow blocks
|
||||
// - Hello Block
|
||||
// - World Block
|
||||
await utils.editor.focusToEditor(page);
|
||||
await page.keyboard.insertText('Hello Block');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.insertText('World Block');
|
||||
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
// Focus Doc Title
|
||||
// - Hello<cursor />
|
||||
await focusDocTitle(page);
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.insert();
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
const editorContent = await utils.editor.getEditorContent(page);
|
||||
expect(editorContent).toBe(`Hello Block\nWorld Block\n${content}`);
|
||||
}).toPass({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should insert content at the end of the note when no block is selected in edgeless mode', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.editor.switchToEdgelessMode(page);
|
||||
await utils.editor.focusToEditor(page);
|
||||
await page.keyboard.insertText('Hello Block');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.insertText('World Block');
|
||||
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.insert();
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
const noteContent = await utils.editor.getNoteContent(page);
|
||||
expect(noteContent).toBe(`Hello Block\nWorld Block\n${content}`);
|
||||
}).toPass({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should create a new note when no block or note is selected in edgeless mode', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.editor.switchToEdgelessMode(page);
|
||||
|
||||
// Delete default note
|
||||
await (await page.waitForSelector('affine-edgeless-note')).click();
|
||||
page.keyboard.press('Delete');
|
||||
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.insert();
|
||||
|
||||
await expect(async () => {
|
||||
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
const noteContent = await utils.editor.getNoteContent(page);
|
||||
expect(noteContent).toBe(content);
|
||||
}).toPass({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIInsertion/SaveAsBlock', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should save content as a chat block in page mode', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.saveAsBlock();
|
||||
|
||||
// Switch to edgeless mode
|
||||
await utils.editor.isEdgelessMode(page);
|
||||
|
||||
// Verify the ai block is created
|
||||
await page.waitForSelector('affine-edgeless-ai-chat');
|
||||
const aiBlock = await page.locator('affine-edgeless-ai-chat');
|
||||
await expect(aiBlock).toBeVisible();
|
||||
});
|
||||
|
||||
test('should save content as a chat block in edgeless mode', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.editor.switchToEdgelessMode(page);
|
||||
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.saveAsBlock();
|
||||
await page.getByText('Successfully saved chat to a block');
|
||||
|
||||
// Verify the ai block is created
|
||||
await page.waitForSelector('affine-edgeless-ai-chat');
|
||||
const aiBlock = await page.locator('affine-edgeless-ai-chat');
|
||||
await expect(aiBlock).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../base/base-test';
|
||||
|
||||
test.describe('AIInsertion/SaveAsDoc', () => {
|
||||
test.beforeEach(async ({ page, utils }) => {
|
||||
const user = await utils.testUtils.getUser();
|
||||
await loginUser(page, user);
|
||||
await utils.testUtils.setupTestEnvironment(page);
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
});
|
||||
|
||||
test('should save content as a doc in page mode', async ({ page, utils }) => {
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for the assistant answer to be completely rendered
|
||||
await page.waitForTimeout(1000);
|
||||
const { actions, content } =
|
||||
await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.saveAsDoc();
|
||||
await page.getByText('New doc created');
|
||||
|
||||
// Verify the ai block is created
|
||||
const editorContent = await utils.editor.getEditorContent(page);
|
||||
expect(editorContent).toBe(content);
|
||||
});
|
||||
|
||||
test('should save content as a doc in edgeless mode', async ({
|
||||
page,
|
||||
utils,
|
||||
}) => {
|
||||
await utils.editor.switchToEdgelessMode(page);
|
||||
|
||||
await utils.chatPanel.openChatPanel(page);
|
||||
await utils.chatPanel.makeChat(page, 'Hello');
|
||||
|
||||
await utils.chatPanel.waitForHistory(page, [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for the assistant answer to be completely rendered
|
||||
await page.waitForTimeout(1000);
|
||||
const { actions, content } =
|
||||
await utils.chatPanel.getLatestAssistantMessage(page);
|
||||
await actions.saveAsDoc();
|
||||
await page.getByText('New doc created');
|
||||
|
||||
// Switch to page mode
|
||||
await utils.editor.isPageMode(page);
|
||||
|
||||
// Verify the ai block is created
|
||||
const editorContent = await utils.editor.getEditorContent(page);
|
||||
expect(editorContent).toBe(content);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
interface Window {
|
||||
showOpenFilePicker?: () => Promise<FileSystemFileHandle[]>;
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// eslint-disable eslint-plugin-unicorn(prefer-dom-node-dataset
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
type ChatStatus = 'loading' | 'success' | 'error' | 'idle' | 'transmitting';
|
||||
|
||||
type ChatUserMessage = {
|
||||
role: 'user';
|
||||
content: string;
|
||||
};
|
||||
|
||||
type ChatAssistantMessage = {
|
||||
role: 'assistant';
|
||||
status: ChatStatus;
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
type ChatActionMessage = {
|
||||
role: 'action';
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
type ChatMessage = ChatUserMessage | ChatAssistantMessage | ChatActionMessage;
|
||||
|
||||
export class ChatPanelUtils {
|
||||
public static async openChatPanel(page: Page) {
|
||||
if (await page.getByTestId('sidebar-tab-chat').isHidden()) {
|
||||
await page.getByTestId('right-sidebar-toggle').click({
|
||||
delay: 200,
|
||||
});
|
||||
}
|
||||
await page.getByTestId('sidebar-tab-chat').click();
|
||||
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
|
||||
}
|
||||
|
||||
public static async typeChat(page: Page, content: string) {
|
||||
await page.getByTestId('chat-panel-input').focus();
|
||||
await page.keyboard.type(content);
|
||||
}
|
||||
|
||||
public static async typeChatSequentially(page: Page, content: string) {
|
||||
const input = await page.locator('chat-panel-input textarea').nth(0);
|
||||
await input.pressSequentially(content, {
|
||||
delay: 50,
|
||||
});
|
||||
}
|
||||
|
||||
public static async makeChat(page: Page, content: string) {
|
||||
await this.openChatPanel(page);
|
||||
await this.typeChat(page, content);
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
public static async clearChat(page: Page) {
|
||||
await page.getByTestId('chat-panel-clear').click();
|
||||
await page.getByTestId('confirm-modal-confirm').click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
public static async collectHistory(page: Page) {
|
||||
return await page.evaluate(() => {
|
||||
const chatPanel = document.querySelector<HTMLElement>(
|
||||
'[data-testid="chat-panel-messages"]'
|
||||
);
|
||||
if (!chatPanel) {
|
||||
return [] as ChatMessage[];
|
||||
}
|
||||
const messages = chatPanel.querySelectorAll<HTMLElement>(
|
||||
'chat-message-user,chat-message-assistant,chat-message-action'
|
||||
);
|
||||
|
||||
return Array.from(messages).map(m => {
|
||||
const isAssistant = m.dataset.testid === 'chat-message-assistant';
|
||||
const isChatAction = m.dataset.testid === 'chat-message-action';
|
||||
|
||||
const isUser = !isAssistant && !isChatAction;
|
||||
|
||||
if (isUser) {
|
||||
return {
|
||||
role: 'user' as const,
|
||||
content:
|
||||
m.querySelector<HTMLElement>(
|
||||
'[data-testid="chat-content-pure-text"]'
|
||||
)?.innerText || '',
|
||||
};
|
||||
}
|
||||
|
||||
if (isAssistant) {
|
||||
return {
|
||||
role: 'assistant' as const,
|
||||
status: m.dataset.status as ChatStatus,
|
||||
title: m.querySelector<HTMLElement>('.user-info')?.innerText || '',
|
||||
content:
|
||||
m.querySelector<HTMLElement>('chat-content-rich-text editor-host')
|
||||
?.innerText || '',
|
||||
};
|
||||
}
|
||||
|
||||
// Must be chat action at this point
|
||||
return {
|
||||
role: 'action' as const,
|
||||
title: m.querySelector<HTMLElement>('.user-info')?.innerText || '',
|
||||
content:
|
||||
m.querySelector<HTMLElement>('chat-content-rich-text editor-host')
|
||||
?.innerText || '',
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static expectHistory(
|
||||
history: ChatMessage[],
|
||||
expected: (
|
||||
| Partial<ChatUserMessage>
|
||||
| Partial<ChatAssistantMessage>
|
||||
| Partial<ChatActionMessage>
|
||||
)[]
|
||||
) {
|
||||
expect(history).toHaveLength(expected.length);
|
||||
history.forEach((message, index) => {
|
||||
const expectedMessage = expected[index];
|
||||
expect(message).toMatchObject(expectedMessage);
|
||||
});
|
||||
}
|
||||
|
||||
public static async expectToHaveHistory(
|
||||
page: Page,
|
||||
expected: (
|
||||
| Partial<ChatUserMessage>
|
||||
| Partial<ChatAssistantMessage>
|
||||
| Partial<ChatActionMessage>
|
||||
)[]
|
||||
) {
|
||||
const history = await this.collectHistory(page);
|
||||
this.expectHistory(history, expected);
|
||||
}
|
||||
|
||||
public static async waitForHistory(
|
||||
page: Page,
|
||||
expected: (
|
||||
| Partial<ChatUserMessage>
|
||||
| Partial<ChatAssistantMessage>
|
||||
| Partial<ChatActionMessage>
|
||||
)[],
|
||||
timeout = 2 * 60000
|
||||
) {
|
||||
await expect(async () => {
|
||||
const history = await this.collectHistory(page);
|
||||
this.expectHistory(history, expected);
|
||||
}).toPass({ timeout });
|
||||
}
|
||||
|
||||
public static async getLatestAssistantMessage(page: Page) {
|
||||
const message = page.getByTestId('chat-message-assistant').last();
|
||||
const actions = await message.getByTestId('chat-actions');
|
||||
const actionList = await message.getByTestId('chat-action-list');
|
||||
return {
|
||||
message,
|
||||
content: await message
|
||||
.locator('chat-content-rich-text editor-host')
|
||||
.innerText(),
|
||||
actions: {
|
||||
copy: async () => actions.getByTestId('action-copy-button').click(),
|
||||
retry: async () => actions.getByTestId('action-retry-button').click(),
|
||||
insert: async () => actionList.getByTestId('action-insert').click(),
|
||||
saveAsBlock: async () =>
|
||||
actionList.getByTestId('action-save-as-block').click(),
|
||||
saveAsDoc: async () =>
|
||||
actionList.getByTestId('action-save-as-doc').click(),
|
||||
addAsNote: async () =>
|
||||
actionList.getByTestId('action-add-to-edgeless-as-note').click(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static async getLatestAIActionMessage(page: Page) {
|
||||
const message = page.getByTestId('chat-message-action').last();
|
||||
const actionName = await message.getByTestId('action-name');
|
||||
await actionName.click();
|
||||
const answer = await message.getByTestId('answer-prompt');
|
||||
const prompt = await message.getByTestId('chat-message-action-prompt');
|
||||
return {
|
||||
message,
|
||||
answer,
|
||||
prompt,
|
||||
actionName,
|
||||
};
|
||||
}
|
||||
|
||||
public static async chatWithDoc(page: Page, docName: string) {
|
||||
const withButton = await page.getByTestId('chat-panel-with-button');
|
||||
await withButton.click();
|
||||
const withMenu = await page.getByTestId('ai-add-popover');
|
||||
await withMenu.getByText(docName).click();
|
||||
await page.getByTestId('chat-panel-chips').getByText(docName);
|
||||
}
|
||||
|
||||
public static async chatWithAttachments(
|
||||
page: Page,
|
||||
attachments: { name: string; mimeType: string; buffer: Buffer }[],
|
||||
text: string
|
||||
) {
|
||||
await page.evaluate(() => {
|
||||
delete window.showOpenFilePicker;
|
||||
});
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
const withButton = await page.getByTestId('chat-panel-with-button');
|
||||
await withButton.click();
|
||||
const withMenu = await page.getByTestId('ai-add-popover');
|
||||
await withMenu.getByTestId('ai-chat-with-files').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(attachment);
|
||||
}
|
||||
await expect(async () => {
|
||||
const states = await page
|
||||
.getByTestId('chat-panel-chip')
|
||||
.evaluateAll(elements =>
|
||||
elements.map(el => el.getAttribute('data-state'))
|
||||
);
|
||||
await expect(states).toHaveLength(attachments.length);
|
||||
await expect(states.every(state => state === 'finished')).toBe(true);
|
||||
}).toPass({ timeout: 20000 });
|
||||
|
||||
await page.pause();
|
||||
await this.makeChat(page, text);
|
||||
}
|
||||
|
||||
public static async chatWithImages(
|
||||
page: Page,
|
||||
images: { name: string; mimeType: string; buffer: Buffer }[],
|
||||
text: string
|
||||
) {
|
||||
await page.evaluate(() => {
|
||||
delete window.showOpenFilePicker;
|
||||
});
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
// Open file upload dialog
|
||||
await page.getByTestId('chat-panel-input-image-upload').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(images);
|
||||
|
||||
await page.waitForSelector('chat-panel-input img');
|
||||
await this.makeChat(page, text);
|
||||
}
|
||||
|
||||
public static async enableNetworkSearch(page: Page) {
|
||||
const networkSearch = await page.getByTestId('chat-network-search');
|
||||
if ((await networkSearch.getAttribute('data-active')) === 'false') {
|
||||
await networkSearch.click();
|
||||
}
|
||||
}
|
||||
|
||||
public static async disableNetworkSearch(page: Page) {
|
||||
const networkSearch = await page.getByTestId('chat-network-search');
|
||||
if ((await networkSearch.getAttribute('data-active')) === 'true') {
|
||||
await networkSearch.click();
|
||||
}
|
||||
}
|
||||
|
||||
public static async isNetworkSearchEnabled(page: Page) {
|
||||
const networkSearch = await page.getByTestId('chat-network-search');
|
||||
return (await networkSearch.getAttribute('aria-disabled')) === 'false';
|
||||
}
|
||||
|
||||
public static async isImageUploadEnabled(page: Page) {
|
||||
const imageUpload = await page.getByTestId('chat-panel-input-image-upload');
|
||||
const disabled = await imageUpload.getAttribute('data-disabled');
|
||||
return disabled === 'false';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
import {
|
||||
createEdgelessNoteBlock,
|
||||
setEdgelessTool,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
pressEscape,
|
||||
selectAllByKeyboard,
|
||||
} from '@affine-test/kit/utils/keyboard';
|
||||
import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic';
|
||||
import type { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
|
||||
import type {
|
||||
MindmapElementModel,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
import { type Page } from '@playwright/test';
|
||||
export class EditorUtils {
|
||||
public static async focusToEditor(page: Page) {
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
public static async getEditorContent(page: Page) {
|
||||
let content = '';
|
||||
let retry = 3;
|
||||
while (!content && retry > 0) {
|
||||
const lines = await page.$$('page-editor .inline-editor');
|
||||
const contents = await Promise.all(lines.map(el => el.innerText()));
|
||||
content = contents
|
||||
.map(c => c.replace(/\u200B/g, '').trim())
|
||||
.filter(c => !!c)
|
||||
.join('\n');
|
||||
if (!content) {
|
||||
await page.waitForTimeout(500);
|
||||
retry -= 1;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public static async getNoteContent(page: Page) {
|
||||
const edgelessNode = await page.waitForSelector(
|
||||
'affine-edgeless-note .edgeless-note-page-content'
|
||||
);
|
||||
return (await edgelessNode.innerText()).replace(/\u200B/g, '').trim();
|
||||
}
|
||||
|
||||
public static async switchToEdgelessMode(page: Page) {
|
||||
const editor = await page.waitForSelector('page-editor');
|
||||
await page.getByTestId('switch-edgeless-mode-button').click();
|
||||
editor.waitForElementState('hidden');
|
||||
await page.waitForSelector('edgeless-editor');
|
||||
}
|
||||
|
||||
public static async switchToPageMode(page: Page) {
|
||||
await page.getByTestId('switch-page-mode-button').click();
|
||||
await page.waitForSelector('page-editor');
|
||||
}
|
||||
|
||||
public static async isPageMode(page: Page) {
|
||||
return await page.waitForSelector('page-editor');
|
||||
}
|
||||
|
||||
public static async isEdgelessMode(page: Page) {
|
||||
return await page.waitForSelector('edgeless-editor');
|
||||
}
|
||||
|
||||
public static async getDocTitle(page: Page) {
|
||||
return page.getByTestId('title-edit-button').innerText();
|
||||
}
|
||||
|
||||
public static async waitForAiAnswer(page: Page) {
|
||||
const answer = await page.getByTestId('ai-penel-answer');
|
||||
await answer.waitFor({
|
||||
state: 'visible',
|
||||
timeout: 2 * 60000,
|
||||
});
|
||||
return answer;
|
||||
}
|
||||
|
||||
private static createAction(page: Page, action: () => Promise<void>) {
|
||||
return async () => {
|
||||
await action();
|
||||
const responses = new Set<string>();
|
||||
const answer = await this.waitForAiAnswer(page);
|
||||
const responsesMenu = answer.getByTestId('answer-responses');
|
||||
await responsesMenu.isVisible();
|
||||
await responsesMenu.scrollIntoViewIfNeeded({ timeout: 60000 });
|
||||
|
||||
if (await responsesMenu.getByTestId('answer-insert-below').isVisible()) {
|
||||
responses.add('insert-below');
|
||||
}
|
||||
if (await responsesMenu.getByTestId('answer-insert-above').isVisible()) {
|
||||
responses.add('insert-above');
|
||||
}
|
||||
if (await responsesMenu.getByTestId('answer-replace').isVisible()) {
|
||||
responses.add('replace-selection');
|
||||
}
|
||||
if (
|
||||
await responsesMenu.getByTestId('answer-use-as-caption').isVisible()
|
||||
) {
|
||||
responses.add('use-as-caption');
|
||||
}
|
||||
if (
|
||||
await responsesMenu.getByTestId('answer-create-new-note').isVisible()
|
||||
) {
|
||||
responses.add('create-new-note');
|
||||
}
|
||||
|
||||
return {
|
||||
answer: await this.waitForAiAnswer(page),
|
||||
responses,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
public static async createEdgelessText(page: Page, text: string) {
|
||||
await setEdgelessTool(page, 'text');
|
||||
await page.mouse.click(400, 400);
|
||||
await page.locator('affine-edgeless-text').waitFor({ state: 'visible' });
|
||||
await page.waitForTimeout(100);
|
||||
const texts = text.split('\n');
|
||||
for (const [index, line] of texts.entries()) {
|
||||
await page.keyboard.insertText(line);
|
||||
if (index !== texts.length - 1) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async createEdgelessNote(page: Page, text: string) {
|
||||
await createEdgelessNoteBlock(page, [500, 300]);
|
||||
const texts = text.split('\n');
|
||||
for (const [index, line] of texts.entries()) {
|
||||
await page.keyboard.insertText(line);
|
||||
if (index !== texts.length - 1) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async createMindmap(page: Page) {
|
||||
await page.keyboard.press('m');
|
||||
await page.mouse.click(400, 400);
|
||||
const id = await page.evaluate(() => {
|
||||
const edgelessBlock = document.querySelector(
|
||||
'affine-edgeless-root'
|
||||
) as EdgelessRootBlockComponent;
|
||||
if (!edgelessBlock) {
|
||||
throw new Error('edgeless block not found');
|
||||
}
|
||||
const mindmaps = edgelessBlock.gfx.gfxElements.filter(
|
||||
(el: GfxModel) => 'type' in el && el.type === 'mindmap'
|
||||
);
|
||||
|
||||
return mindmaps[mindmaps.length - 1].id;
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public static async getMindMapNode(
|
||||
page: Page,
|
||||
mindmapId: string,
|
||||
path: number[]
|
||||
) {
|
||||
return page.evaluate(
|
||||
({ mindmapId, path }) => {
|
||||
const edgelessBlock = document.querySelector(
|
||||
'affine-edgeless-root'
|
||||
) as EdgelessRootBlockComponent;
|
||||
if (!edgelessBlock) {
|
||||
throw new Error('edgeless block not found');
|
||||
}
|
||||
|
||||
const mindmap = edgelessBlock.gfx.getElementById(
|
||||
mindmapId
|
||||
) as MindmapElementModel;
|
||||
if (!mindmap) {
|
||||
throw new Error(`Mindmap not found: ${mindmapId}`);
|
||||
}
|
||||
|
||||
const node = mindmap.getNodeByPath(path);
|
||||
if (!node) {
|
||||
throw new Error(`Mindmap node not found at: ${path}`);
|
||||
}
|
||||
|
||||
const rect = edgelessBlock.gfx.viewport.toViewBound(
|
||||
node.element.elementBound
|
||||
);
|
||||
|
||||
return {
|
||||
path: mindmap.getPath(node),
|
||||
id: node.id,
|
||||
text: (node.element as ShapeElementModel).text?.toString() ?? '',
|
||||
rect: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
w: rect.w,
|
||||
h: rect.h,
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
mindmapId,
|
||||
path,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static async selectElementInEdgeless(page: Page, elements: string[]) {
|
||||
await page.evaluate(
|
||||
({ elements }) => {
|
||||
const edgelessBlock = document.querySelector(
|
||||
'affine-edgeless-root'
|
||||
) as EdgelessRootBlockComponent;
|
||||
if (!edgelessBlock) {
|
||||
throw new Error('edgeless block not found');
|
||||
}
|
||||
|
||||
edgelessBlock.gfx.selection.set({
|
||||
elements,
|
||||
});
|
||||
},
|
||||
{ elements }
|
||||
);
|
||||
}
|
||||
|
||||
public static async askAIWithEdgeless(
|
||||
page: Page,
|
||||
createBlock: () => Promise<void>,
|
||||
afterSelected?: () => Promise<void>
|
||||
) {
|
||||
await this.switchToEdgelessMode(page);
|
||||
await selectAllByKeyboard(page);
|
||||
await page.keyboard.press('Delete');
|
||||
await createBlock();
|
||||
await pressEscape(page, 5);
|
||||
await selectAllByKeyboard(page);
|
||||
await afterSelected?.();
|
||||
await page.getByTestId('ask-ai-button').click();
|
||||
|
||||
return {
|
||||
aiImageFilter: this.createAction(page, () =>
|
||||
page.getByTestId('action-ai-image-filter').click()
|
||||
),
|
||||
brainstorm: this.createAction(page, () =>
|
||||
page.getByTestId('action-brainstorm').click()
|
||||
),
|
||||
brainstormMindMap: this.createAction(page, () =>
|
||||
page.getByTestId('action-brainstorm-mindmap').click()
|
||||
),
|
||||
changeTone: (
|
||||
tone: 'professional' | 'informal' | 'friendly' | 'critical' | 'humorous'
|
||||
) =>
|
||||
this.createAction(page, async () => {
|
||||
await page.getByTestId('action-change-tone').hover();
|
||||
await page.getByTestId(`action-change-tone-${tone}`).click();
|
||||
})(),
|
||||
checkCodeError: this.createAction(page, () =>
|
||||
page.getByTestId('action-check-code-error').click()
|
||||
),
|
||||
continueWithAi: async () => {
|
||||
page.getByTestId('action-continue-with-ai').click();
|
||||
},
|
||||
continueWriting: this.createAction(page, () =>
|
||||
page.getByTestId('action-continue-writing').click()
|
||||
),
|
||||
createHeadings: this.createAction(page, () =>
|
||||
page.getByTestId('action-create-headings').click()
|
||||
),
|
||||
explainSelection: this.createAction(page, () =>
|
||||
page.getByTestId('action-explain-selection').click()
|
||||
),
|
||||
findActions: this.createAction(page, () =>
|
||||
page.getByTestId('action-find-actions').click()
|
||||
),
|
||||
fixGrammar: this.createAction(page, () =>
|
||||
page.getByTestId('action-fix-grammar').click()
|
||||
),
|
||||
fixSpelling: this.createAction(page, () =>
|
||||
page.getByTestId('action-fix-spelling').click()
|
||||
),
|
||||
generateCaption: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-caption').click()
|
||||
),
|
||||
generateHeadings: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-headings').click()
|
||||
),
|
||||
generateImage: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-image').click()
|
||||
),
|
||||
generateOutline: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-outline').click()
|
||||
),
|
||||
generatePresentation: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-presentation').click()
|
||||
),
|
||||
imageProcessing: this.createAction(page, () =>
|
||||
page.getByTestId('action-image-processing').click()
|
||||
),
|
||||
improveGrammar: this.createAction(page, () =>
|
||||
page.getByTestId('action-improve-grammar').click()
|
||||
),
|
||||
improveWriting: this.createAction(page, () =>
|
||||
page.getByTestId('action-improve-writing').click()
|
||||
),
|
||||
makeItLonger: this.createAction(page, () =>
|
||||
page.getByTestId('action-make-it-longer').click()
|
||||
),
|
||||
makeItReal: this.createAction(page, () =>
|
||||
page.getByTestId('action-make-it-real').click()
|
||||
),
|
||||
makeItShorter: this.createAction(page, () =>
|
||||
page.getByTestId('action-make-it-shorter').click()
|
||||
),
|
||||
summarize: this.createAction(page, () =>
|
||||
page.getByTestId('action-summarize').click()
|
||||
),
|
||||
translate: (language: string) =>
|
||||
this.createAction(page, async () => {
|
||||
await page.getByTestId('action-translate').hover();
|
||||
await page.getByTestId(`action-translate-${language}`).click();
|
||||
})(),
|
||||
writeArticle: this.createAction(page, () =>
|
||||
page.getByTestId('action-write-article').click()
|
||||
),
|
||||
writeBlogPost: this.createAction(page, () =>
|
||||
page.getByTestId('action-write-blog-post').click()
|
||||
),
|
||||
writePoem: this.createAction(page, () =>
|
||||
page.getByTestId('action-write-poem').click()
|
||||
),
|
||||
writeTwitterPost: this.createAction(page, () =>
|
||||
page.getByTestId('action-write-twitter-post').click()
|
||||
),
|
||||
regenerateMindMap: this.createAction(page, () =>
|
||||
page.getByTestId('action-regenerate-mindmap').click()
|
||||
),
|
||||
expandMindMapNode: async () =>
|
||||
page.getByTestId('action-expand-mindmap-node').click(),
|
||||
} as const;
|
||||
}
|
||||
|
||||
public static async askAIWithCode(
|
||||
page: Page,
|
||||
code: string,
|
||||
language: string
|
||||
) {
|
||||
await this.focusToEditor(page);
|
||||
await page.keyboard.insertText(`\`\`\`${language}`);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.insertText(code);
|
||||
await page.locator('affine-code').blur();
|
||||
await page.locator('affine-code').hover();
|
||||
await page.getByTestId('ask-ai-button').click();
|
||||
return {
|
||||
explainCode: this.createAction(page, () =>
|
||||
page.getByTestId('action-explain-code').click()
|
||||
),
|
||||
checkCodeError: this.createAction(page, () =>
|
||||
page.getByTestId('action-check-code-error').click()
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
public static async askAIWithImage(
|
||||
page: Page,
|
||||
image: { name: string; mimeType: string; buffer: Buffer }
|
||||
) {
|
||||
await page.evaluate(() => {
|
||||
delete window.showOpenFilePicker;
|
||||
});
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
|
||||
await this.focusToEditor(page);
|
||||
await page.keyboard.press('/');
|
||||
await page.keyboard.insertText('image');
|
||||
await page.locator('affine-slash-menu').getByTestId('Image').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(image);
|
||||
|
||||
await page.locator('affine-page-image').click();
|
||||
await page.getByTestId('ask-ai-button').click();
|
||||
|
||||
return {
|
||||
explainImage: this.createAction(page, () =>
|
||||
page.getByTestId('action-explain-image').click()
|
||||
),
|
||||
generateImage: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-image').click()
|
||||
),
|
||||
generateCaption: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-caption').click()
|
||||
),
|
||||
imageProcessing: (type: string) =>
|
||||
this.createAction(page, async () => {
|
||||
await page.getByTestId('action-image-processing').hover();
|
||||
await page.getByTestId(`action-image-processing-${type}`).click();
|
||||
})(),
|
||||
imageFilter: (style: string) =>
|
||||
this.createAction(page, async () => {
|
||||
await page.getByTestId('action-ai-image-filter').hover();
|
||||
await page.getByTestId(`action-image-filter-${style}`).click();
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
public static async askAIWithText(page: Page, text: string) {
|
||||
await this.focusToEditor(page);
|
||||
const texts = text.split('\n');
|
||||
for (const [index, line] of texts.entries()) {
|
||||
await page.keyboard.insertText(line);
|
||||
if (index !== texts.length - 1) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
await page.keyboard.press('ControlOrMeta+A');
|
||||
await page.keyboard.press('ControlOrMeta+A');
|
||||
await page.keyboard.press('ControlOrMeta+A');
|
||||
|
||||
const askAI = await page.locator('page-editor editor-toolbar ask-ai-icon');
|
||||
await askAI.waitFor({
|
||||
state: 'attached',
|
||||
timeout: 5000,
|
||||
});
|
||||
await askAI.click();
|
||||
|
||||
return {
|
||||
aiImageFilter: this.createAction(page, () =>
|
||||
page.getByTestId('action-ai-image-filter').click()
|
||||
),
|
||||
brainstorm: this.createAction(page, () =>
|
||||
page.getByTestId('action-brainstorm').click()
|
||||
),
|
||||
brainstormMindMap: this.createAction(page, () =>
|
||||
page.getByTestId('action-brainstorm-mindmap').click()
|
||||
),
|
||||
changeTone: (
|
||||
tone: 'professional' | 'informal' | 'friendly' | 'critical' | 'humorous'
|
||||
) =>
|
||||
this.createAction(page, async () => {
|
||||
await page.getByTestId('action-change-tone').hover();
|
||||
await page.getByTestId(`action-change-tone-${tone}`).click();
|
||||
})(),
|
||||
checkCodeError: this.createAction(page, () =>
|
||||
page.getByTestId('action-check-code-error').click()
|
||||
),
|
||||
continueWithAi: async () => {
|
||||
page.getByTestId('action-continue-with-ai').click();
|
||||
},
|
||||
continueWriting: this.createAction(page, () =>
|
||||
page.getByTestId('action-continue-writing').click()
|
||||
),
|
||||
createHeadings: this.createAction(page, () =>
|
||||
page.getByTestId('action-create-headings').click()
|
||||
),
|
||||
explainSelection: this.createAction(page, () =>
|
||||
page.getByTestId('action-explain-selection').click()
|
||||
),
|
||||
findActions: this.createAction(page, () =>
|
||||
page.getByTestId('action-find-actions').click()
|
||||
),
|
||||
fixGrammar: this.createAction(page, () =>
|
||||
page.getByTestId('action-fix-grammar').click()
|
||||
),
|
||||
fixSpelling: this.createAction(page, () =>
|
||||
page.getByTestId('action-fix-spelling').click()
|
||||
),
|
||||
generateCaption: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-caption').click()
|
||||
),
|
||||
generateHeadings: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-headings').click()
|
||||
),
|
||||
generateImage: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-image').click()
|
||||
),
|
||||
generateOutline: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-outline').click()
|
||||
),
|
||||
generatePresentation: this.createAction(page, () =>
|
||||
page.getByTestId('action-generate-presentation').click()
|
||||
),
|
||||
imageProcessing: this.createAction(page, () =>
|
||||
page.getByTestId('action-image-processing').click()
|
||||
),
|
||||
improveGrammar: this.createAction(page, () =>
|
||||
page.getByTestId('action-improve-grammar').click()
|
||||
),
|
||||
improveWriting: this.createAction(page, () =>
|
||||
page.getByTestId('action-improve-writing').click()
|
||||
),
|
||||
makeItLonger: this.createAction(page, () =>
|
||||
page.getByTestId('action-make-it-longer').click()
|
||||
),
|
||||
makeItReal: this.createAction(page, () =>
|
||||
page.getByTestId('action-make-it-real').click()
|
||||
),
|
||||
makeItShorter: this.createAction(page, () =>
|
||||
page.getByTestId('action-make-it-shorter').click()
|
||||
),
|
||||
summarize: this.createAction(page, () =>
|
||||
page.getByTestId('action-summarize').click()
|
||||
),
|
||||
translate: (language: string) =>
|
||||
this.createAction(page, async () => {
|
||||
await page.getByTestId('action-translate').hover();
|
||||
await page.getByTestId(`action-translate-${language}`).click();
|
||||
})(),
|
||||
writeArticle: this.createAction(page, () =>
|
||||
page.getByTestId('action-write-article').click()
|
||||
),
|
||||
writeBlogPost: this.createAction(page, () =>
|
||||
page.getByTestId('action-write-blog-post').click()
|
||||
),
|
||||
writePoem: this.createAction(page, () =>
|
||||
page.getByTestId('action-write-poem').click()
|
||||
),
|
||||
writeTwitterPost: this.createAction(page, () =>
|
||||
page.getByTestId('action-write-twitter-post').click()
|
||||
),
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { createRandomAIUser } from '@affine-test/kit/utils/cloud';
|
||||
import { openHomePage, setCoreUrl } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
doc: Store;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestUtils {
|
||||
private static instance: TestUtils;
|
||||
private isProduction: boolean;
|
||||
|
||||
private constructor() {
|
||||
this.isProduction = process.env.NODE_ENV === 'production';
|
||||
if (
|
||||
process.env.PLAYWRIGHT_USER_AGENT &&
|
||||
process.env.PLAYWRIGHT_EMAIL &&
|
||||
!process.env.PLAYWRIGHT_PASSWORD
|
||||
) {
|
||||
setCoreUrl(process.env.PLAYWRIGHT_CORE_URL || 'http://localhost:8080');
|
||||
this.isProduction = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): TestUtils {
|
||||
if (!TestUtils.instance) {
|
||||
TestUtils.instance = new TestUtils();
|
||||
}
|
||||
return TestUtils.instance;
|
||||
}
|
||||
|
||||
public getUser() {
|
||||
if (
|
||||
!this.isProduction ||
|
||||
!process.env.PLAYWRIGHT_EMAIL ||
|
||||
!process.env.PLAYWRIGHT_PASSWORD
|
||||
) {
|
||||
return createRandomAIUser();
|
||||
}
|
||||
|
||||
return {
|
||||
email: process.env.PLAYWRIGHT_EMAIL,
|
||||
password: process.env.PLAYWRIGHT_PASSWORD,
|
||||
};
|
||||
}
|
||||
|
||||
public async setupTestEnvironment(page: Page) {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
}
|
||||
|
||||
public async createTestWorkspace(page: Page, name: string = 'test') {
|
||||
await createLocalWorkspace({ name }, page);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user