Skip to content
Merged
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
21 changes: 10 additions & 11 deletions backend/src/core/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ async def initialize_database(self):
"""Initialize the database schema and seed data."""
try:
db_manager = self.container.db_manager()

# In test mode, clean up the database on startup
# This ensures tests don't affect each other
if APP_ENV == "test":
try:
logger.info("Test mode detected: Cleaning up database on startup")
await db_manager.drop_all()
logger.info("Test database cleaned up")
except Exception as e:
logger.error(f"Failed to clean up test database: {str(e)}")

await db_manager.create_all()
await initialize_data()
Expand Down Expand Up @@ -55,17 +65,6 @@ async def shutdown(self):
billing_worker.stop()
logger.info("Billing worker stopped")

# In test mode, clean up the database on shutdown
# This ensures tests don't affect each other
if APP_ENV == "test":
try:
logger.info("Test mode detected: Cleaning up database on shutdown")
db_manager = self.container.db_manager()
await db_manager.drop_all()
logger.info("Test database cleaned up")
except Exception as e:
logger.error(f"Failed to clean up test database: {str(e)}")

# Close database connections
try:
db_manager = self.container.db_manager()
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/Instance/Setting/AccessMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const AccessMenu: React.FC = () => {
endIcon={showPassword ? <EyeSlashIcon className="w-5 h-5" /> : <EyeIcon className="w-5 h-5" />}
onEndIconClick={togglePasswordVisibility}
disabled={!isInstanceRunning}
data-testid="reset-root-password"
/>
</div>

Expand All @@ -97,6 +98,7 @@ const AccessMenu: React.FC = () => {
variant="outline"
icon={<KeyIcon className="w-5 h-5" />}
disabled={!isInstanceRunning || !isPasswordValid(password)}
data-testid="reset-root-password"
/>
</Section>
)
Expand All @@ -119,6 +121,7 @@ const AccessMenu: React.FC = () => {
tabs={accessTabs}
activeTab={accessType}
setActiveTab={(id) => setAccessType(id as 'terminal' | 'console')}
data-testid="instance-access"
/>

<div className="bg-[#12203c] rounded-lg overflow-hidden shadow-lg border border-blue-900/20">
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/Instance/Setting/DestroyMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ExclamationTriangleIcon, TrashIcon } from "@heroicons/react/24/outline"
import Section from "../../../components/Common/Section"
import Button from "../../../components/Common/Button/Button"
import { useInstanceSetting } from "../../../hooks/Instance/useInstanceSetting"
import InputField from "../../Common/InputField"

const DestroyMenu: React.FC = () => {
const [confirmText, setConfirmText] = useState("")
Expand Down Expand Up @@ -47,12 +48,12 @@ const DestroyMenu: React.FC = () => {
<label className="block text-gray-300 mb-2 text-sm">
To confirm, type the instance name: <span className="font-bold">{instanceName}</span>
</label>
<input
<InputField
type="text"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
className="bg-[#23375F] border border-blue-800/30 text-white px-3 py-2 rounded w-full focus:outline-none focus:ring-2 focus:ring-red-500"
onChange={setConfirmText}
placeholder={`Type ${instanceName} to confirm`}
data-testid="destroy-instance-input"
/>
</div>

Expand All @@ -63,6 +64,7 @@ const DestroyMenu: React.FC = () => {
icon={<TrashIcon className="w-5 h-5" />}
variant="purple"
disabled={!isDeleteConfirmed || isLoading}
data-testid="destroy-instance"
/>
</div>
</Section>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useEffect } from 'react'
import { useConsoleWebSocket } from '../../../hooks/Instance/useConsoleConnection'
import { useXTerm } from '../../../hooks/useXTerm'
import { ArrowPathIcon } from '@heroicons/react/24/outline'
import { WS_URL } from '../../../config/api'
import 'xterm/css/xterm.css'
import { StatusHeader, StatusMessage, StatusFooter } from './InstanceStatus'
Expand Down Expand Up @@ -79,6 +78,7 @@ const InstanceConsole: React.FC<InstanceConsoleProps> = ({ instanceName, isRunni
<div
ref={terminalRef}
className="p-2"
data-testid="instance-console-container"
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const InstanceTerminal: React.FC<InstanceTerminalProps> = ({ instanceName, isRun
<div
ref={terminalRef}
className="p-2"
data-testid="instance-terminal-container"
/>
</div>

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Instance/Setting/MonitorMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const MonitorMenu: React.FC = () => {
</div>
<div className="flex justify-between text-sm">
<span className="text-white">{cpu.usage.toFixed(1)}%</span>
<span className="text-gray-400">{cpu.cores} core{cpu.cores !== 1 ? 's' : ''}</span>
<span className="text-gray-400" data-testid="cpu-cores">{cpu.cores} core{cpu.cores !== 1 ? 's' : ''}</span>
</div>
</div>
</div>
Expand All @@ -62,7 +62,7 @@ const MonitorMenu: React.FC = () => {
</div>
<div className="flex justify-between text-sm">
<span className="text-white">{memory.percentage.toFixed(1)}%</span>
<span className="text-gray-400">
<span className="text-gray-400" data-testid="memory-used">
{formatBytes(memory.used)} / {formatBytes(memory.total)}
</span>
</div>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/Instance/Setting/PowerMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ const PowerMenu: React.FC = () => {
onClick={startInstance}
variant="primary"
icon={<PowerIcon className="w-5 h-5" />}
data-testid="start-instance"
/>
) : (
<Button
label="Stop Instance"
onClick={stopInstance}
variant="primary"
icon={<PowerIcon className="w-5 h-5" />}
data-testid="stop-instance"
/>
)}

Expand All @@ -54,6 +56,7 @@ const PowerMenu: React.FC = () => {
variant="outline"
icon={<ArrowPathIcon className="w-5 h-5" />}
disabled={!isInstanceRunning}
data-testid="restart-instance"
/>
</div>
</Section>
Expand Down
8 changes: 3 additions & 5 deletions frontend/src/hooks/useXTerm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,9 @@ export const useXTerm = () => {
}, [])

const setTerminalDataHandlerV2 = useCallback((handler: (data: string) => void) => {
if (xtermRef.current) {
xtermRef.current.onData((data) => {
handler(data)
})
}
xtermRef.current?.onData((data) => {
handler(data)
})
}, [])

// Get current terminal dimensions
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/Instance/InstanceListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const InstanceListPage: React.FC = () => {
handleViewInstance(instance)
}}
variant="secondary"
data-testid={`view-instance-${instance.instance_name}`}
/>
<Button
icon={<EllipsisVerticalIcon className="w-5 h-5" />}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/Instance/InstanceSettingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const InstanceSettingPage: React.FC = () => {
tabs={tabs}
activeTab={activeTab}
setActiveTab={setActiveTab}
data-testid="instance-setting"
/>
<InstanceSettingContent active={activeTab} />
</>
Expand Down
10 changes: 9 additions & 1 deletion test/e2e/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ def login_user(self, user: UserData) -> Optional[APIResponse]:
return self.make_request("user/login", data, action_name="Login")

# Instance Management API Methods
def start_instance(self, hostname: str) -> Optional[APIResponse]:
"""Start an instance by hostname"""
return self.make_request(f"instance/{hostname}/start", method="post", action_name="Start Instance")

def stop_instance(self, hostname: str) -> Optional[APIResponse]:
"""Stop an instance by hostname"""
return self.make_request(f"instance/{hostname}/stop", method="post", action_name="Stop Instance")

def delete_instance(self, hostname: str) -> Optional[APIResponse]:
"""Delete an instance by hostname"""
return self.make_request(f"instance/{hostname}/delete", method="post", action_name="Delete Instance")
Expand Down Expand Up @@ -153,4 +161,4 @@ def create_instance(self, instance_data: UserInstanceData) -> Optional[APIRespon
"cost_hour": instance_data.instance_plan.cost_hour
}
}
return self.make_request("instance/create", data, action_name="Create Instance")
return self.make_request("instance/create", data, action_name="Create Instance")
33 changes: 32 additions & 1 deletion test/e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,41 @@ def test_lifecycle(action_registry: ActionRegistry, page: Page, backend_url: str
"api_client": api_client
}
test_data = action_registry.run_before_actions(**action_context)
page.wait_for_load_state("load", timeout=10000)

# Return the test data (results from before actions)
yield test_data

# Run after actions - using load state instead of the discouraged networkidle
page.wait_for_load_state("load", timeout=10000)
action_registry.run_after_actions(**action_context)
action_registry.run_after_actions(**action_context)


@pytest.fixture(scope="class")
def test_class_lifecycle(browser: Browser, browser_context_args: Dict[str, Any], action_registry: ActionRegistry, backend_url: str, request: pytest.FixtureRequest) -> Generator[TestData, None, None]:
"""
Fixture that runs before_all actions once at the beginning of a test class
and after_all actions once at the end of a test class.

This fixture must be explicitly included in test classes that need class-level setup/teardown.
"""
context = browser.new_context(**browser_context_args)
page = context.new_page()

# Create API client
api_client = ApiClient(page, backend_url)

# Run before_all actions once at the beginning of the class
action_context = {
"page": page,
"backend_url": backend_url,
"request": request,
"api_client": api_client
}
test_data = action_registry.run_before_all_actions(**action_context)

# Return the test data (results from before_all actions)
yield test_data

# Run after_all actions once at the end of the class
action_registry.run_after_all_actions(**action_context)
12 changes: 12 additions & 0 deletions test/e2e/pages/base_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ def wait_for_page_load(self) -> None:
# This method is kept for backward compatibility
# Use wait_for_locator or wait_for_event instead
self.page.wait_for_load_state("load")

def wait_for_document_ready(self) -> None:
"""
Wait for the document to be ready.
"""
self.page.wait_for_load_state("domcontentloaded")

def wait_for_locator(self, locator: Locator, state: str = "visible", timeout: int = 30000) -> Locator:
"""
Expand Down Expand Up @@ -120,6 +126,12 @@ def wait_for_navigation(self, url_pattern: Optional[str] = None, timeout: int =
self.page.wait_for_url(url_pattern, timeout=timeout)
else:
self.page.wait_for_load_state("load", timeout=timeout)

def wait_for_timeout(self, timeout: int = 10000) -> None:
"""
Wait for a timeout.
"""
self.page.wait_for_timeout(timeout)

def wait_for_toast(self, toast_type: str = "success", timeout: int = 10000) -> Locator:
"""
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/pages/instance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

from .instance_list_page import InstanceListPage
from .instance_create_page import InstanceCreatePage
from .instance_setting_page import InstanceSettingPage

__all__ = [
'InstanceListPage',
'InstanceCreatePage'
'InstanceCreatePage',
'InstanceSettingPage'
]
6 changes: 6 additions & 0 deletions test/e2e/pages/instance/instance_list_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ def click_create_instance_button(self):

def should_navigate_to_instance_create_page(self):
expect(self.page).to_have_url(f"/user/{self.username}/instance/create")

def click_row_view_button(self, hostname: str):
self.page.get_by_test_id(f"view-instance-{hostname}-button").click()

def should_navigate_to_instance_setting_page(self, hostname: str):
expect(self.page).to_have_url(f"/user/{self.username}/instance/{hostname}")
Loading