Skip to content

Commit 1c828cd

Browse files
[ENG-1038] e2e tests for Google Sheets & Hutspot (#172)
* Added e2e tests for sqlite * Fixed comments for the datasources * Added sheet tests * Added hubspot tests * Removed readme setup * Created selector helper for api e2e * Fixed test * Fixed PR comments * Fixed PR comments * Fixed tests * Fixed test * Test fix * Updated test name * Renamed files * Updated location of the imports --------- Co-authored-by: David Rojas <david.rojasleiton1000@gmail.com>
1 parent 5724b72 commit 1c828cd

File tree

7 files changed

+409
-0
lines changed

7 files changed

+409
-0
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ jobs:
7474
env:
7575
BASE_URL: http://localhost:3000
7676
CI: true
77+
HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
7778

7879
- name: 📋 Show Docker ai-app container logs after test run
7980
if: always()

app/components/@settings/tabs/data/forms/AddDataSourceForm.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ export default function AddDataSourceForm({ isSubmitting, setIsSubmitting, onSuc
378378
value={dbName}
379379
onChange={(e) => setDbName(e.target.value)}
380380
disabled={isSubmitting}
381+
data-testid="data-source-name-input"
381382
className={classNames(
382383
'w-full px-4 py-2.5 bg-[#F5F5F5] dark:bg-gray-700 border rounded-lg',
383384
'text-primary placeholder-tertiary text-base',
@@ -400,6 +401,7 @@ export default function AddDataSourceForm({ isSubmitting, setIsSubmitting, onSuc
400401
value={propertyValues[property.type] || ''}
401402
onChange={(e) => handlePropertyChange(property.type, e.target.value)}
402403
disabled={isSubmitting}
404+
data-testid={'data-source-token-input'}
403405
className={classNames(
404406
'w-full px-4 py-2.5 bg-[#F5F5F5] dark:bg-gray-700 border rounded-lg',
405407
'text-primary placeholder-tertiary text-base',
@@ -470,6 +472,7 @@ export default function AddDataSourceForm({ isSubmitting, setIsSubmitting, onSuc
470472
await handleTestConnection();
471473
}}
472474
disabled={isFormDisabled}
475+
data-testid="test-connection-button"
473476
className={classNames(
474477
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
475478
'bg-depth-1 bg-depth-1/50 ',
@@ -496,6 +499,7 @@ export default function AddDataSourceForm({ isSubmitting, setIsSubmitting, onSuc
496499
type="button"
497500
onClick={handleSubmit}
498501
disabled={isFormDisabled}
502+
data-testid="create-datasource-button"
499503
className={classNames(
500504
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
501505
'bg-accent-500 hover:bg-accent-600',

app/components/@settings/tabs/data/forms/GoogleSheetsSetup.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ export default function GoogleSheetsSetup({ onSuccess, environmentId }: GoogleSh
659659
value={dataSourceName}
660660
onChange={(e) => setDataSourceName(e.target.value)}
661661
placeholder="Enter data source name"
662+
data-testid="data-source-name-input"
662663
style={{
663664
borderRadius: '8px',
664665
padding: '8px 12px',
@@ -770,6 +771,7 @@ export default function GoogleSheetsSetup({ onSuccess, environmentId }: GoogleSh
770771
value={googleSheetsUrl}
771772
onChange={(e) => setGoogleSheetsUrl(e.target.value)}
772773
placeholder="https://docs.google.com/spreadsheets/d/..."
774+
data-testid="google-sheets-url-input"
773775
style={{
774776
background: 'var(--color-gray-600)',
775777
color: '#fff',
File renamed without changes.

tests/e2e/helpers/selectors.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Page } from '@playwright/test';
2+
3+
/**
4+
* Common selectors used across multiple test files
5+
*/
6+
export const getDataSourceNameInput = (page: Page) => {
7+
return page.locator('[data-testid="data-source-name-input"]');
8+
};
9+
10+
export const getAccessTokenInput = (page: Page) => {
11+
return page.locator('[data-testid="data-source-token-input"]');
12+
};
13+
14+
export const getGoogleSheetsUrlInput = (page: Page) => {
15+
return page.locator('[data-testid="google-sheets-url-input"]');
16+
};
17+
18+
export const getCreateButton = (page: Page) => {
19+
return page.locator('[data-testid="create-datasource-button"]');
20+
};
21+
22+
export const getTestConnectionButton = (page: Page) => {
23+
return page.locator('[data-testid="test-connection-button"]');
24+
};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { type Page, test, expect } from '@playwright/test';
2+
import { performInitialSetup } from '../helpers/setup';
3+
import { getDataSourceNameInput, getGoogleSheetsUrlInput, getCreateButton } from '../helpers/selectors';
4+
import { navigateToDataSourceForm, navigateToSettings } from '../helpers/navigate';
5+
6+
test.describe('Add Google Sheets Data Source Flow', () => {
7+
test.beforeEach(async ({ page }) => {
8+
test.setTimeout(180000);
9+
await performInitialSetup(page);
10+
await navigateToSettings(page);
11+
await navigateToDataSourceForm(page, 'google-sheets');
12+
});
13+
14+
test('Create Google Sheets data source with public spreadsheet URL', async ({ page }: { page: Page }) => {
15+
const dbNameInput = getDataSourceNameInput(page);
16+
await dbNameInput.waitFor({ state: 'visible', timeout: 10000 });
17+
await dbNameInput.fill('test-google-sheets');
18+
19+
// Look for Google Sheets specific inputs - URL or spreadsheet ID
20+
const urlInput = getGoogleSheetsUrlInput(page);
21+
22+
const urlInputExists = await urlInput.isVisible({ timeout: 5000 }).catch(() => false);
23+
24+
if (urlInputExists) {
25+
// Use a simple public Google Sheets document that doesn't require authentication
26+
await urlInput.fill(
27+
'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit?usp=sharing',
28+
);
29+
}
30+
31+
// Look for OAuth connect button or Google authentication
32+
const connectButton = page
33+
.locator('[data-testid="google-oauth-connect"]')
34+
.or(
35+
page.getByRole('button', { name: /connect to google|authenticate|sign in with google|connect google sheets/i }),
36+
)
37+
.or(page.locator('button:has-text("Connect to Google"), button:has-text("Authenticate")'));
38+
39+
const connectButtonExists = await connectButton.isVisible({ timeout: 5000 }).catch(() => false);
40+
41+
if (connectButtonExists) {
42+
// Note: In a real test environment, we would mock the OAuth flow
43+
await connectButton.click();
44+
await page.waitForLoadState('networkidle');
45+
}
46+
47+
const saveButton = getCreateButton(page);
48+
49+
// Wait for the button with more tolerance for OAuth flows
50+
const saveButtonExists = await saveButton.isVisible({ timeout: 15000 }).catch(() => false);
51+
52+
if (!saveButtonExists) {
53+
console.log('Create button not found - this may be expected for OAuth-based data sources');
54+
return; // Exit gracefully as OAuth flow may not be completable in test environment
55+
}
56+
57+
await saveButton.waitFor({ state: 'visible', timeout: 15000 });
58+
59+
// Check if button is enabled (might require OAuth completion)
60+
const buttonEnabled = await saveButton.isEnabled({ timeout: 5000 }).catch(() => false);
61+
62+
if (!buttonEnabled) {
63+
return;
64+
}
65+
66+
// Wait for the API response to validate successful data source creation
67+
const [response] = await Promise.all([
68+
page.waitForResponse(
69+
(response) => response.url().includes('/api/data-sources') && response.request().method() === 'POST',
70+
),
71+
saveButton.click(),
72+
]);
73+
74+
// Validate API response status
75+
if (response.status() === 200 || response.status() === 201) {
76+
// Wait for UI to update after successful API call
77+
await page.waitForLoadState('networkidle');
78+
79+
// Look for success message in UI as confirmation
80+
const successMessage = page.getByText(/successfully|success|created/i).first();
81+
await successMessage.waitFor({ state: 'visible', timeout: 5000 });
82+
} else {
83+
throw new Error(`Data source creation failed with status: ${response.status()}`);
84+
}
85+
});
86+
87+
test('Validate Google Sheets OAuth flow and required fields', async ({ page }: { page: Page }) => {
88+
const saveButton = getCreateButton(page);
89+
90+
// Wait for the button with more tolerance for OAuth flows
91+
const saveButtonExists = await saveButton.isVisible({ timeout: 15000 }).catch(() => false);
92+
93+
if (!saveButtonExists) {
94+
// If no create button is found, the form might require OAuth completion first
95+
console.log('Create button not found - this may be expected for OAuth-based data sources');
96+
return; // Exit gracefully as OAuth flow may not be completable in test environment
97+
}
98+
99+
await saveButton.waitFor({ state: 'visible', timeout: 15000 });
100+
101+
// Test 1: Try to create without filling required fields (but only if button is initially disabled)
102+
const initiallyDisabled = await saveButton.isDisabled({ timeout: 3000 }).catch(() => false);
103+
104+
if (initiallyDisabled) {
105+
await expect(saveButton, 'Create button should be disabled when required fields are empty').toBeDisabled();
106+
}
107+
108+
// Test 2: Fill name but leave OAuth/URL empty
109+
const dbNameInput = getDataSourceNameInput(page);
110+
await dbNameInput.waitFor({ state: 'visible', timeout: 10000 });
111+
await dbNameInput.fill('test-google-sheets-validation');
112+
113+
// Assert that Create button is still disabled without OAuth/connection (may vary by implementation)
114+
const buttonStillDisabled = await saveButton.isDisabled({ timeout: 3000 }).catch(() => false);
115+
116+
if (buttonStillDisabled) {
117+
await expect(saveButton, 'Create button should remain disabled without OAuth').toBeDisabled();
118+
}
119+
120+
// Test 3: Test OAuth connection functionality
121+
const connectButton = page
122+
.locator('[data-testid="google-oauth-connect"]')
123+
.or(page.getByRole('button', { name: /connect to google|authenticate|sign in with google/i }))
124+
.or(page.locator('button:has-text("Connect to Google"), button:has-text("Authenticate")'));
125+
126+
const connectButtonExists = await connectButton.isVisible({ timeout: 5000 }).catch(() => false);
127+
128+
if (connectButtonExists) {
129+
// Test OAuth connection with API response validation
130+
const [authResponse] = await Promise.all([
131+
page.waitForResponse(
132+
(response) =>
133+
response.url().includes('/auth/google') ||
134+
response.url().includes('/oauth') ||
135+
response.url().includes('/google-sheets'),
136+
),
137+
connectButton.click(),
138+
]);
139+
140+
// Wait for OAuth flow completion
141+
await page.waitForLoadState('networkidle');
142+
143+
if (authResponse.status() === 200 || authResponse.status() === 302) {
144+
// In a real test environment, we would mock the OAuth callback
145+
const buttonEnabledAfterOAuth = await saveButton.isEnabled({ timeout: 5000 }).catch(() => false);
146+
147+
if (buttonEnabledAfterOAuth) {
148+
await expect(saveButton, 'Create button should be enabled after OAuth').toBeEnabled();
149+
}
150+
}
151+
}
152+
153+
// Test 4: Test direct URL input if available
154+
const urlInput = getGoogleSheetsUrlInput(page);
155+
156+
const urlInputExists = await urlInput.isVisible({ timeout: 5000 }).catch(() => false);
157+
158+
if (urlInputExists) {
159+
// Test invalid URL
160+
await urlInput.fill('invalid-url');
161+
162+
// Check if validation prevents button enabling (but don't fail if it doesn't)
163+
const stillDisabledWithInvalidUrl = await saveButton.isDisabled({ timeout: 3000 }).catch(() => false);
164+
165+
if (stillDisabledWithInvalidUrl) {
166+
await expect(saveButton, 'Create button should remain disabled with invalid URL').toBeDisabled();
167+
}
168+
169+
// Test valid URL - use public sharing URL
170+
await urlInput.fill(
171+
'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit?usp=sharing',
172+
);
173+
174+
// Check if button becomes enabled with valid URL (may depend on OAuth)
175+
const buttonEnabledWithUrl = await saveButton.isEnabled({ timeout: 3000 }).catch(() => false);
176+
177+
if (buttonEnabledWithUrl) {
178+
await expect(saveButton, 'Create button should be enabled with valid URL').toBeEnabled();
179+
}
180+
}
181+
});
182+
183+
test('Test Google Sheets OAuth error handling', async ({ page }: { page: Page }) => {
184+
// Fill in the data source name
185+
const dbNameInput = getDataSourceNameInput(page);
186+
await dbNameInput.waitFor({ state: 'visible', timeout: 10000 });
187+
await dbNameInput.fill('test-oauth-error-handling');
188+
189+
// Look for OAuth connect button
190+
const connectButton = page
191+
.locator('[data-testid="google-oauth-connect"]')
192+
.or(page.getByRole('button', { name: /connect to google|authenticate|sign in with google/i }))
193+
.or(page.locator('button:has-text("Connect to Google"), button:has-text("Authenticate")'));
194+
195+
const connectButtonExists = await connectButton.isVisible({ timeout: 5000 }).catch(() => false);
196+
197+
if (connectButtonExists) {
198+
// Test OAuth connection and handle potential errors
199+
const [authResponse] = await Promise.all([
200+
page.waitForResponse(
201+
(response) =>
202+
response.url().includes('/auth/google') ||
203+
response.url().includes('/oauth') ||
204+
response.url().includes('/google-sheets') ||
205+
response.status() >= 400, // Capture error responses
206+
),
207+
connectButton.click(),
208+
]);
209+
210+
if (authResponse.status() >= 400) {
211+
// Look for error message in UI (may or may not appear)
212+
const errorMessage = page.getByText(/error|failed|unable to connect|authentication failed/i).first();
213+
await errorMessage.isVisible({ timeout: 3000 }).catch(() => false);
214+
215+
// Verify that Create button remains disabled after OAuth error
216+
const saveButton = getCreateButton(page);
217+
218+
const buttonStillDisabled = await saveButton.isDisabled({ timeout: 3000 }).catch(() => false);
219+
220+
if (buttonStillDisabled) {
221+
await expect(saveButton, 'Create button should remain disabled after OAuth error').toBeDisabled();
222+
}
223+
}
224+
} else {
225+
// If no OAuth button exists, just validate the form persists data correctly
226+
console.log('No OAuth button found - skipping OAuth error handling test');
227+
}
228+
229+
// Test form state persistence
230+
const currentName = await dbNameInput.inputValue();
231+
expect(currentName).toBe('test-oauth-error-handling');
232+
});
233+
});

0 commit comments

Comments
 (0)