Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cypress e2e test #74

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Cypress Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
cypress-run:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
cache: 'pip'

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: web/package-lock.json

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -e .

- name: Install Node dependencies
working-directory: web
run: npm ci

- name: Create test data directory
shell: bash
run: |
mkdir -p ~/latent-scope-data
ls-init ~/latent-scope-data

- name: Start backend server
shell: bash
run: |
ls-serve &

- name: Start frontend dev server
working-directory: web
shell: bash
run: |
npm run dev &

- name: Wait and check server ports
shell: bash
run: |
echo "Checking if servers are running..."

# Try for 30 seconds, checking every second
for i in {1..30}; do
backend_up=false
frontend_up=false

# Check backend server
if curl -s http://localhost:5001/health > /dev/null; then
echo "✓ Backend server running on port 5001"
backend_up=true
else
echo "Backend server not yet running on port 5001..."
fi

# Check frontend server
if curl -s http://localhost:5174 > /dev/null; then
echo "✓ Frontend server running on port 5174"
frontend_up=true
else
echo "Frontend server not yet running on port 5174..."
fi

# If both are up, we can exit successfully
if [ "$backend_up" = true ] && [ "$frontend_up" = true ]; then
echo "Both servers are up!"
exit 0
fi

# If we haven't succeeded yet, sleep and try again
if [ $i -lt 30 ]; then
echo "Waiting 1 second before retry..."
sleep 1
fi
done

# If we get here, we've timed out
echo "Timed out waiting for servers to start"
exit 1

- name: Run Cypress tests
id: cypress
uses: cypress-io/github-action@v6
with:
working-directory: web
browser: chrome
config: baseUrl=http://localhost:5174
env:
CYPRESS_BASE_URL: http://localhost:5174
CYPRESS_API_URL: http://localhost:5001

- name: Upload test screenshots
uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-screenshots
path: web/cypress/screenshots
retention-days: 30
8 changes: 8 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ npm run dev
This sets up a local development server for the client code, typically at http://localhost:5173
This will call the local API at http://localhost:5001 as set in `web/.env.development`

### Running Cypress tests

This will run an E2E cypress test runner in a browser window.
Make sure that both latentscope and the webapp are running.

```
npm run cypress:open
```

## Building for distribution
You can build a new version of the module, this will package the latest version of the web interface as well.
Expand Down
18 changes: 18 additions & 0 deletions web/cypress.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from 'cypress'

export default defineConfig({
e2e: {
baseUrl: 'http://localhost:5174',
setupNodeEvents(on, config) {
// implement node event listeners here
config.env = {
...config.env,
apiUrl: process.env.CYPRESS_API_URL || 'http://localhost:5001'
}
return config
},
screenshotOnRunFailure: true,
video: false,
screenshotsFolder: 'cypress/screenshots',
},
})
113 changes: 113 additions & 0 deletions web/cypress/e2e/app.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
describe('Latent Scope App', () => {
beforeEach(() => {
// Visit the app before each test
cy.visit('/')
})

afterEach(function() {
cy.screenshot(this.currentTest.title)
})

it('loads the home page successfully', () => {
// Check that main app container exists
cy.get('.page').should('exist')

cy.get('.home').contains('Create new dataset')
cy.get('.section.datasets').contains('Datasets')
})

it('shows navigation menu', () => {
// Check that navigation exists and contains expected links
cy.get('nav').should('exist')
})

it('can navigate to settings page', () => {
// Click settings link and verify page loads
cy.get('a[href="/settings"]').click()
cy.get('.settings').should('exist')
cy.get('[class*="_header_"]').should('contain', 'Settings')
})

it('displays API settings correctly', () => {
cy.visit('/settings')

// Check that settings sections exist
cy.get('[class*="_dot-env_"]').should('exist')
cy.get('[class*="_data-dir_"]').should('exist')
cy.get('[class*="_api-keys_"]').should('exist')
})

it('can update OpenAI API key', () => {
cy.visit('/settings')

// Verify API call was made correctly
cy.intercept('POST', '/api/settings').as('updateSettings')

// Find the OpenAI API key input
cy.contains('OPENAI_API_KEY')
.parent()
.within(() => {
// Type the test API key
cy.get('input[type="password"]').type('test-api-key-123')
// Submit the form
cy.get('form').submit()
})

// Verify the success state (green checkmark appears)
cy.contains('OPENAI_API_KEY')
.parent()
.should('contain', '✅')

cy.wait('@updateSettings').then((interception) => {
expect(interception.request.body).to.have.property('OPENAI_API_KEY', 'test-api-key-123')
})
})

it('handles API errors gracefully', () => {
// Intercept API call and simulate error
cy.intercept('GET', '/api/settings', {
statusCode: 500,
body: 'Server error'
})

cy.visit('/settings')

// Verify error state is handled
cy.get('.settings').should('exist')
})

it('can create new dataset and see it on homepage', () => {
// Visit home page
cy.visit('/')

// Select and upload file
cy.get('#upload-button').selectFile('cypress/fixtures/example.csv', { force: true })

// Dataset name should be auto-populated from filename
cy.get('#dataset-name').should('have.value', 'example')

// Submit form
cy.get('.new-dataset form button[type="submit"]').click()

// Wait for redirect after submitting form
cy.url().should('include', '/setup')

// Verify setup page shows all required steps
cy.contains('Embed').should('exist')
cy.contains('UMAP').should('exist')
cy.contains('Cluster').should('exist')
cy.contains('Label Clusters').should('exist')
cy.contains('Scope').should('exist')
cy.contains('Select embedding model:').should('exist')

// Go back to homepage
cy.visit('/')

// Verify the new dataset appears in the datasets list
cy.get('.section.datasets')
.should('exist')
.within(() => {
cy.contains('example').should('exist')
})
})
})
11 changes: 11 additions & 0 deletions web/cypress/fixtures/example.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
text,label
"This movie was absolutely amazing! The acting and storyline were perfect.",positive
"I really didn't enjoy this product, waste of money.",negative
"The service was okay, nothing special but not bad either.",neutral
"Terrible experience, would not recommend to anyone.",negative
"The new features are fantastic and really improve usability!",positive
"Not sure how I feel about the changes, some good some bad.",neutral
"Best purchase I've made this year, totally worth it!",positive
"Mediocre performance, meets basic requirements.",neutral
"Complete disaster, poor quality and horrible customer service.",negative
"Love it! Can't imagine using anything else now.",positive
1 change: 1 addition & 0 deletions web/cypress/screenshots/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.png
43 changes: 43 additions & 0 deletions web/cypress/support/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************

// Track console errors
let consoleErrors = [];

Cypress.on('window:console', (msg) => {
if (msg.type === 'error') {
consoleErrors.push(msg);
}
});

// Clear errors before each test
beforeEach(() => {
consoleErrors = [];
});

// Check for errors after each test
afterEach(() => {
if (consoleErrors.length > 0) {
const errorMessages = consoleErrors.map(error => error.message || error).join('\n');
throw new Error(`Console errors detected:\n${errorMessages}`);
}
});

// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })

// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })

// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })

// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
15 changes: 15 additions & 0 deletions web/cypress/support/e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Import commands.js using ES modules syntax
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')

// Hide fetch/XHR requests from command log
const app = window.top;
if (app && !app.document.head.querySelector('[data-hide-command-log-request]')) {
const style = app.document.createElement('style');
style.innerHTML =
'.command-name-request, .command-name-xhr { display: none }';
style.setAttribute('data-hide-command-log-request', '');
app.document.head.appendChild(style);
}
Loading
Loading