diff --git a/cypress.config.ts b/cypress.config.ts index 04a855a5..aa72b4bf 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -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; 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/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", 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() {