From cc81d9974e6b353fec3f5bf14173a60d246db0ef Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 29 Sep 2025 16:09:25 +0800 Subject: [PATCH 1/3] test: chat input test --- cypress.config.ts | 16 + cypress/e2e/chat/chat-input.cy.ts | 275 ++++++++++++++++++ cypress/e2e/chat/selection-mode.cy.ts | 189 ++++++++++++ .../chat/components/chat-input/index.tsx | 53 ++-- .../chat-input/related-views/index.tsx | 3 +- .../chat/components/ui/format-group.tsx | 2 +- 6 files changed, 511 insertions(+), 27 deletions(-) create mode 100644 cypress/e2e/chat/chat-input.cy.ts create mode 100644 cypress/e2e/chat/selection-mode.cy.ts diff --git a/cypress.config.ts b/cypress.config.ts index 04a855a5..36dd4aa1 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -23,6 +23,22 @@ export default defineConfig({ viewportWidth: 1440, viewportHeight: 900, setupNodeEvents(on, config) { + // Configure browser launch options + on('before:browser:launch', (browser, launchOptions) => { + if (browser.family === 'chromium' && browser.name !== 'electron') { + // Remove --start-fullscreen flag and set window size + launchOptions.args = launchOptions.args.filter(arg => !arg.includes('--start-fullscreen')); + launchOptions.args.push('--window-size=1440,900'); + launchOptions.args.push('--window-position=100,100'); + + // Disable kiosk mode + const kioskIndex = launchOptions.args.indexOf('--kiosk'); + if (kioskIndex > -1) { + launchOptions.args.splice(kioskIndex, 1); + } + } + return launchOptions; + }); // Override baseUrl if CYPRESS_BASE_URL is set if (process.env.CYPRESS_BASE_URL) { config.baseUrl = process.env.CYPRESS_BASE_URL; diff --git a/cypress/e2e/chat/chat-input.cy.ts b/cypress/e2e/chat/chat-input.cy.ts new file mode 100644 index 00000000..5d090eb7 --- /dev/null +++ b/cypress/e2e/chat/chat-input.cy.ts @@ -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', ''); + }); + }); +}); \ No newline at end of file diff --git a/cypress/e2e/chat/selection-mode.cy.ts b/cypress/e2e/chat/selection-mode.cy.ts new file mode 100644 index 00000000..09a6241f --- /dev/null +++ b/cypress/e2e/chat/selection-mode.cy.ts @@ -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'); + }); + }); +}); diff --git a/src/components/chat/components/chat-input/index.tsx b/src/components/chat/components/chat-input/index.tsx index ea391e34..c1ea31b6 100644 --- a/src/components/chat/components/chat-input/index.tsx +++ b/src/components/chat/components/chat-input/index.tsx @@ -192,18 +192,19 @@ export function ChatInput() {
- - @@ -216,18 +217,19 @@ export function ChatInput() { - - @@ -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 ? ( @@ -273,4 +276,4 @@ export function ChatInput() {
); -} \ No newline at end of file +} diff --git a/src/components/chat/components/chat-input/related-views/index.tsx b/src/components/chat/components/chat-input/related-views/index.tsx index 59303b3d..f4f8390a 100644 --- a/src/components/chat/components/chat-input/related-views/index.tsx +++ b/src/components/chat/components/chat-input/related-views/index.tsx @@ -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' > {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' > ); } - diff --git a/src/components/chat/components/ui/format-group.tsx b/src/components/chat/components/ui/format-group.tsx index 6613986a..01218a10 100644 --- a/src/components/chat/components/ui/format-group.tsx +++ b/src/components/chat/components/ui/format-group.tsx @@ -110,7 +110,7 @@ export function FormatGroup({ }; return ( -
+
{renderGroup(actions, false)}
From 377b9998b7cb4225e210ad2f0f03cfe645dcc38a Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 30 Sep 2025 11:56:47 +0800 Subject: [PATCH 2/3] test: chat input test --- cypress.config.ts | 33 +++++++++++++++++++++++---------- package.json | 2 ++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index 36dd4aa1..c4d7c7ee 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -25,17 +25,30 @@ export default defineConfig({ setupNodeEvents(on, config) { // Configure browser launch options on('before:browser:launch', (browser, launchOptions) => { - if (browser.family === 'chromium' && browser.name !== 'electron') { - // Remove --start-fullscreen flag and set window size - launchOptions.args = launchOptions.args.filter(arg => !arg.includes('--start-fullscreen')); - launchOptions.args.push('--window-size=1440,900'); - launchOptions.args.push('--window-position=100,100'); + 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'); + }); - // Disable kiosk mode - const kioskIndex = launchOptions.args.indexOf('--kiosk'); - if (kioskIndex > -1) { - launchOptions.args.splice(kioskIndex, 1); - } + // 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; }); diff --git a/package.json b/package.json index 629e7749..3ce2bd05 100644 --- a/package.json +++ b/package.json @@ -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", From 814753b0ed6bb8e0d802d8278e592bada76e11d6 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 30 Sep 2025 15:19:17 +0800 Subject: [PATCH 3/3] test: lint --- cypress.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress.config.ts b/cypress.config.ts index c4d7c7ee..aa72b4bf 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -50,6 +50,7 @@ export default defineConfig({ console.log('Chrome launch args:', launchOptions.args); } + return launchOptions; }); // Override baseUrl if CYPRESS_BASE_URL is set