Merge pull request #89 from AppFlowy-IO/chat_test2

Chat test2
This commit is contained in:
Nathan.fooo
2025-09-30 15:44:17 +08:00
committed by GitHub
7 changed files with 527 additions and 27 deletions

View File

@@ -23,6 +23,36 @@ export default defineConfig({
viewportWidth: 1440,
viewportHeight: 900,
setupNodeEvents(on, config) {
// Configure browser launch options
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' || browser.family === 'chromium') {
// Remove fullscreen and kiosk related flags
launchOptions.args = launchOptions.args.filter(arg => {
return !arg.includes('--start-fullscreen') &&
!arg.includes('--start-maximized') &&
!arg.includes('--kiosk') &&
!arg.includes('--app') &&
!arg.includes('--auto-open-devtools-for-tabs');
});
// Add flags to ensure windowed mode
// Position window at bottom of screen (adjust based on your screen height)
// For a 1080p screen (1920x1080), positioning at y=180 leaves the window at bottom
// For a 1440p screen (2560x1440), positioning at y=540 leaves the window at bottom
launchOptions.args.push('--window-size=1440,900');
launchOptions.args.push('--window-position=0,180');
launchOptions.args.push('--disable-gpu-sandbox');
launchOptions.args.push('--no-sandbox');
launchOptions.args.push('--disable-dev-shm-usage');
// Force disable fullscreen
launchOptions.args.push('--force-device-scale-factor=1');
console.log('Chrome launch args:', launchOptions.args);
}
return launchOptions;
});
// Override baseUrl if CYPRESS_BASE_URL is set
if (process.env.CYPRESS_BASE_URL) {
config.baseUrl = process.env.CYPRESS_BASE_URL;

View File

@@ -0,0 +1,275 @@
import { v4 as uuidv4 } from 'uuid';
import { AuthTestUtils } from '../../support/auth-utils';
import { TestTool } from '../../support/page-utils';
import { PageSelectors, SidebarSelectors } from '../../support/selectors';
describe('Chat Input Tests', () => {
const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL');
const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL');
const generateRandomEmail = () => `${uuidv4()}@appflowy.io`;
let testEmail: string;
before(() => {
cy.task(
'log',
`Test Environment Configuration:\n - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL}\n - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`
);
});
beforeEach(() => {
testEmail = generateRandomEmail();
});
it('tests chat input UI controls', () => {
cy.on('uncaught:exception', (err: Error) => {
if (err.message.includes('No workspace or service found') ||
err.message.includes('View not found') ||
err.message.includes('WebSocket') ||
err.message.includes('connection') ||
err.message.includes('Failed to load models') ||
err.message.includes('Minified React error')) {
return false;
}
return true;
});
cy.visit('/login', { failOnStatusCode: false });
cy.wait(2000);
const authUtils = new AuthTestUtils();
authUtils.signInWithTestUrl(testEmail).then(() => {
cy.url().should('include', '/app');
SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 });
PageSelectors.items().should('exist', { timeout: 30000 });
cy.wait(2000);
TestTool.expandSpace();
cy.wait(1000);
PageSelectors.items()
.first()
.trigger('mouseenter', { force: true })
.trigger('mouseover', { force: true });
cy.wait(1000);
cy.get('[data-testid="inline-add-page"]').first().click({ force: true });
cy.get('[data-testid="add-ai-chat-button"]').should('be.visible').click();
cy.wait(2000);
// Test 1: Format toggle
cy.log('Testing format toggle');
cy.get('body').then($body => {
if ($body.find('[data-testid="chat-format-group"]').length > 0) {
cy.get('[data-testid="chat-input-format-toggle"]').click();
cy.get('[data-testid="chat-format-group"]').should('not.exist');
}
});
cy.get('[data-testid="chat-input-format-toggle"]').should('be.visible').click();
cy.get('[data-testid="chat-format-group"]').should('exist');
cy.get('[data-testid="chat-format-group"] button').should('have.length.at.least', 4);
cy.get('[data-testid="chat-input-format-toggle"]').click();
cy.get('[data-testid="chat-format-group"]').should('not.exist');
// Test 2: Model selector
cy.log('Testing model selector');
cy.get('[data-testid="model-selector-button"]').should('be.visible').click();
cy.get('[data-testid^="model-option-"]').should('exist');
cy.get('body').click(0, 0);
// Test 3: Browse prompts
cy.log('Testing browse prompts');
cy.get('[data-testid="chat-input-browse-prompts"]').click();
cy.get('[role="dialog"]').should('exist');
cy.get('[role="dialog"]').contains('Browse prompts').should('be.visible');
cy.get('body').type('{esc}');
cy.get('[role="dialog"]').should('not.exist');
// Test 4: Related views
cy.log('Testing related views');
cy.get('[data-testid="chat-input-related-views"]').click();
cy.get('[data-testid="chat-related-views-popover"]').should('be.visible');
cy.get('body').type('{esc}');
cy.get('[data-testid="chat-related-views-popover"]').should('not.exist');
});
});
it('tests chat input message handling', () => {
cy.on('uncaught:exception', (err: Error) => {
if (err.message.includes('No workspace or service found') ||
err.message.includes('View not found') ||
err.message.includes('WebSocket') ||
err.message.includes('connection') ||
err.message.includes('Failed to load models') ||
err.message.includes('Minified React error')) {
return false;
}
return true;
});
cy.visit('/login', { failOnStatusCode: false });
cy.wait(2000);
const authUtils = new AuthTestUtils();
authUtils.signInWithTestUrl(testEmail).then(() => {
cy.url().should('include', '/app');
SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 });
PageSelectors.items().should('exist', { timeout: 30000 });
cy.wait(2000);
TestTool.expandSpace();
cy.wait(1000);
PageSelectors.items()
.first()
.trigger('mouseenter', { force: true })
.trigger('mouseover', { force: true });
cy.wait(1000);
cy.get('[data-testid="inline-add-page"]').first().click({ force: true });
cy.get('[data-testid="add-ai-chat-button"]').should('be.visible').click();
cy.wait(3000); // Wait for chat to fully load
// Mock API endpoints with more realistic responses
cy.intercept('POST', '**/api/chat/**/message/question', (req) => {
req.reply({
statusCode: 200,
body: {
code: 0,
data: {
message_id: Date.now().toString(),
content: req.body.content || 'Test message',
chat_id: 'test-chat-id',
},
message: 'success',
},
});
}).as('submitQuestion');
cy.intercept('POST', '**/api/chat/**/answer/stream', (req) => {
req.reply({
statusCode: 200,
body: 'data: {"content":"Test response","type":"message"}\n\n',
headers: {
'content-type': 'text/event-stream',
},
});
}).as('streamAnswer');
// Get the textarea using a more flexible selector
const getTextarea = () => cy.get('textarea').first();
// Test 1: Check textarea exists and is ready
cy.log('Testing textarea availability');
getTextarea().should('exist').and('be.visible');
// Test 2: Keyboard interactions with better waits
cy.log('Testing keyboard interactions');
getTextarea()
.should('not.be.disabled')
.clear()
.type('First line')
.should((el) => {
expect(el.val()).to.include('First line');
});
cy.wait(500);
getTextarea()
.type('{shift+enter}Second line')
.should((el) => {
expect(el.val()).to.include('First line\nSecond line');
});
// Test 3: Textarea auto-resize
cy.log('Testing auto-resize');
getTextarea().then($textarea => {
const initialHeight = $textarea.height();
cy.wrap($textarea)
.clear()
.type('Line 1{shift+enter}Line 2{shift+enter}Line 3{shift+enter}Line 4');
cy.wait(500);
getTextarea().then($resized => {
const newHeight = $resized.height();
cy.log(`Initial height: ${initialHeight}, New height: ${newHeight}`);
expect(newHeight).to.be.at.least(initialHeight!);
});
});
// Test 4: Button states with better selectors
cy.log('Testing button states');
getTextarea().clear();
cy.wait(500);
// Check send button is disabled when empty
cy.get('[data-testid="chat-input-send"]').should('exist');
cy.get('[data-testid="chat-input-send"]').then($button => {
// Button might be disabled via attribute or opacity
const isDisabled = $button.prop('disabled') || $button.css('opacity') === '0.5';
expect(isDisabled).to.be.true;
});
// Type message and check button becomes enabled
getTextarea().type('Test message');
cy.wait(500);
cy.get('[data-testid="chat-input-send"]').then($button => {
const isDisabled = $button.prop('disabled') || $button.css('opacity') === '0.5';
expect(isDisabled).to.be.false;
});
// Test 5: Message sending with proper waits
cy.log('Testing message sending');
getTextarea().clear().type('Hello world');
cy.wait(500);
cy.get('[data-testid="chat-input-send"]').click();
cy.wait('@submitQuestion', { timeout: 10000 });
// Wait for textarea to be ready again
cy.wait(2000);
getTextarea()
.should('exist')
.and('be.visible')
.and('have.value', '');
// Test 6: Special characters with simpler approach
cy.log('Testing special characters');
cy.wait(1000);
const specialMessage = 'Test with special: @#$%';
getTextarea()
.should('not.be.disabled')
.clear()
.type(specialMessage);
cy.wait(500);
getTextarea().should((el) => {
expect(el.val()).to.equal(specialMessage);
});
// Test 7: Enter sends message with better handling
cy.log('Testing Enter key');
getTextarea().clear();
cy.wait(500);
getTextarea().type('Quick test{enter}');
cy.wait('@submitQuestion', { timeout: 10000 });
cy.wait(2000);
getTextarea()
.should('exist')
.and('have.value', '');
});
});
});

View File

@@ -0,0 +1,189 @@
import { v4 as uuidv4 } from 'uuid';
import { AuthTestUtils } from '../../support/auth-utils';
import { TestTool } from '../../support/page-utils';
import { PageSelectors, SidebarSelectors } from '../../support/selectors';
const STUBBED_MESSAGE_ID = 101;
const STUBBED_MESSAGE_CONTENT = 'Stubbed AI answer ready for export';
function setupChatApiStubs() {
cy.intercept('GET', '**/api/chat/**/message**', {
statusCode: 200,
body: {
code: 0,
data: {
messages: [
{
message_id: STUBBED_MESSAGE_ID,
author: {
author_type: 3,
author_uuid: 'assistant',
},
content: STUBBED_MESSAGE_CONTENT,
created_at: new Date().toISOString(),
meta_data: [],
},
],
has_more: false,
total: 1,
},
message: 'success',
},
}).as('getChatMessages');
cy.intercept('GET', '**/api/chat/**/settings**', {
statusCode: 200,
body: {
code: 0,
data: {
rag_ids: [],
metadata: {
ai_model: 'Auto',
},
},
message: 'success',
},
}).as('getChatSettings');
cy.intercept('PATCH', '**/api/chat/**/settings**', {
statusCode: 200,
body: {
code: 0,
message: 'success',
},
}).as('updateChatSettings');
cy.intercept('GET', '**/api/ai/**/model/list**', {
statusCode: 200,
body: {
code: 0,
data: {
models: [
{
name: 'Auto',
metadata: { is_default: true, desc: 'Automatically select an AI model' },
},
{
name: 'E2E Test Model',
provider: 'Test Provider',
metadata: { is_default: false, desc: 'Stubbed model for testing' },
},
],
},
message: 'success',
},
}).as('getModelList');
cy.intercept('GET', '**/api/chat/**/**/related_question**', {
statusCode: 200,
body: {
code: 0,
data: {
message_id: `${STUBBED_MESSAGE_ID}`,
items: [],
},
message: 'success',
},
}).as('getRelatedQuestions');
}
describe('Chat Selection Mode Tests', () => {
const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL');
const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL');
const generateRandomEmail = () => `${uuidv4()}@appflowy.io`;
let testEmail: string;
before(() => {
cy.task('log', `Test Environment Configuration:\n - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL}\n - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`);
});
beforeEach(() => {
testEmail = generateRandomEmail();
setupChatApiStubs();
});
it('enables message selection mode and toggles message selection', () => {
cy.on('uncaught:exception', (err: Error) => {
if (err.message.includes('No workspace or service found')) {
return false;
}
if (err.message.includes('View not found')) {
return false;
}
if (err.message.includes('WebSocket') || err.message.includes('connection')) {
return false;
}
return true;
});
cy.visit('/login', { failOnStatusCode: false });
cy.wait(2000);
const authUtils = new AuthTestUtils();
authUtils.signInWithTestUrl(testEmail).then(() => {
cy.url().should('include', '/app');
SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 });
PageSelectors.items().should('exist', { timeout: 30000 });
cy.wait(2000);
TestTool.expandSpace();
cy.wait(1000);
PageSelectors.items()
.first()
.as('firstSidebarPage');
cy.get('@firstSidebarPage')
.trigger('mouseenter', { force: true })
.trigger('mouseover', { force: true });
cy.wait(1000);
cy.get('[data-testid="inline-add-page"]').first().click({ force: true });
cy.get('[data-testid="add-ai-chat-button"]').should('be.visible').click();
cy.wait('@getChatSettings');
cy.wait('@getModelList');
cy.wait('@getChatMessages');
cy.contains(STUBBED_MESSAGE_CONTENT).should('be.visible');
cy.get('[data-testid="page-more-actions"]').first().click({ force: true });
cy.get('[role="menu"]').should('exist');
cy.contains('[role="menuitem"]', 'Add messages to page')
.should('exist')
.click({ force: true });
cy.get('.chat-selections-banner', { timeout: 10000 })
.should('be.visible')
.and('contain.text', 'Select messages');
cy.get(`[data-message-id="${STUBBED_MESSAGE_ID}"]`).as('firstMessage');
cy.get('@firstMessage')
.find('button.w-4.h-4')
.first()
.click();
cy.get('@firstMessage')
.find('svg.text-primary')
.should('exist');
cy.get('.chat-selections-banner').should('contain.text', '1 selected');
cy.get('.chat-selections-banner')
.find('button')
.last()
.click({ force: true });
cy.get('.chat-selections-banner').should('not.exist');
cy.get('@firstMessage')
.find('button.w-4.h-4')
.should('not.exist');
});
});
});

View File

@@ -20,6 +20,8 @@
"test:unit:coverage": "cross-env COVERAGE=true jest --coverage",
"test:components:coverage": "cross-env COVERAGE=true cypress run --component --browser chrome --headless",
"test:cy": "cypress run",
"test:cy:chrome": "cypress run --browser chrome --headed",
"test:cy:chrome:windowed": "ELECTRON_EXTRA_LAUNCH_ARGS='--window-size=1440,900 --window-position=100,100' cypress run --browser chrome --headed",
"test:integration": "cypress run --spec 'cypress/e2e/**/*.cy.ts'",
"coverage": "cross-env COVERAGE=true pnpm run test:unit && cross-env COVERAGE=true pnpm run test:components",
"generate-tokens": "node scripts/system-token/convert-tokens.cjs",

View File

@@ -192,18 +192,19 @@ export function ChatInput() {
<div className={'flex items-center justify-between gap-4'}>
<div className={'flex items-center gap-1'}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onMouseDown={(e) => {
e.preventDefault();
}}
variant={'ghost'}
size={'icon'}
onClick={() => {
setResponseMode(
responseMode === ChatInputMode.FormatResponse ? ChatInputMode.Auto : ChatInputMode.FormatResponse
);
}}
<TooltipTrigger asChild>
<Button
onMouseDown={(e) => {
e.preventDefault();
}}
variant={'ghost'}
size={'icon'}
data-testid='chat-input-format-toggle'
onClick={() => {
setResponseMode(
responseMode === ChatInputMode.FormatResponse ? ChatInputMode.Auto : ChatInputMode.FormatResponse
);
}}
>
<FormatIcon className='text-icon-secondary h-5 w-5' />
</Button>
@@ -216,18 +217,19 @@ export function ChatInput() {
<ModelSelector className={'h-7'} disabled={questionSending || answerApplying} />
<Tooltip>
<TooltipTrigger asChild>
<Button
onMouseDown={(e) => {
e.preventDefault();
}}
variant={'ghost'}
className={'h-7 text-xs text-text-secondary'}
onClick={() => {
reloadDatabasePrompts();
openModal();
}}
>
<TooltipTrigger asChild>
<Button
onMouseDown={(e) => {
e.preventDefault();
}}
variant={'ghost'}
className={'h-7 text-xs text-text-secondary'}
data-testid='chat-input-browse-prompts'
onClick={() => {
reloadDatabasePrompts();
openModal();
}}
>
{t('chat.customPrompt.browsePrompts')}
</Button>
</TooltipTrigger>
@@ -260,6 +262,7 @@ export function ChatInput() {
variant={'link'}
className={'text-fill-theme-thick p-0'}
disabled={!message.trim() || disabled}
data-testid='chat-input-send'
>
{questionSending ? (
<LoadingDots />
@@ -273,4 +276,4 @@ export function ChatInput() {
</div>
</motion.div>
);
}
}

View File

@@ -86,6 +86,7 @@ export function RelatedViews() {
size='sm'
className='gap-0.5 px-1.5 text-sm text-text-secondary'
variant={'ghost'}
data-testid='chat-input-related-views'
>
<DocIcon className='h-5 w-5 text-icon-secondary' />
{length}
@@ -99,6 +100,7 @@ export function RelatedViews() {
initial="hidden"
animate={open ? "visible" : "exit"}
className={'h-fit min-h-[200px] max-h-[360px] w-[300px] flex flex-col'}
data-testid='chat-related-views-popover'
>
<SearchInput
className='m-2'
@@ -126,4 +128,3 @@ export function RelatedViews() {
</Popover>
);
}

View File

@@ -110,7 +110,7 @@ export function FormatGroup({
};
return (
<div className={'flex gap-2 items-center'}>
<div className={'flex gap-2 items-center'} data-testid='chat-format-group'>
<div className={'flex gap-1'}>
{renderGroup(actions, false)}
</div>