diff --git a/rbox/routers/users.py b/rbox/routers/users.py
index 3636115..9bb2010 100644
--- a/rbox/routers/users.py
+++ b/rbox/routers/users.py
@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends
from ..auth import get_current_user
-from ..models import User_Pydantic, User
+from ..models import User_Pydantic, User, File, Folder
+from typing import List, Dict, Any
router = APIRouter(
prefix="/users",
@@ -10,3 +11,42 @@ router = APIRouter(
@router.get("/me", response_model=User_Pydantic)
async def read_users_me(current_user: User = Depends(get_current_user)):
return await User_Pydantic.from_tortoise_orm(current_user)
+
+@router.get("/me/export", response_model=Dict[str, Any])
+async def export_my_data(current_user: User = Depends(get_current_user)):
+ """
+ Exports all personal data associated with the current user.
+ Includes user profile, and metadata for all owned files and folders.
+ """
+ user_data = await User_Pydantic.from_tortoise_orm(current_user)
+
+ files = await File.filter(owner=current_user).values(
+ "id", "name", "size", "created_at", "modified_at", "file_type", "parent_id"
+ )
+ folders = await Folder.filter(owner=current_user).values(
+ "id", "name", "created_at", "modified_at", "parent_id"
+ )
+
+ return {
+ "user_profile": user_data.dict(),
+ "files_metadata": files,
+ "folders_metadata": folders,
+ # In a more complete implementation, other data like activity logs,
+ # share information, etc., would also be included.
+ }
+
+@router.delete("/me", status_code=204)
+async def delete_my_account(current_user: User = Depends(get_current_user)):
+ """
+ Deletes the current user's account and all associated data.
+ This includes all files and folders owned by the user.
+ """
+ # Delete all files and folders owned by the user
+ await File.filter(owner=current_user).delete()
+ await Folder.filter(owner=current_user).delete()
+
+ # Finally, delete the user account
+ await current_user.delete()
+ return {}
+
+
diff --git a/static/css/style.css b/static/css/style.css
index 3bb2f73..6e1f741 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -998,3 +998,47 @@ body.dark-mode {
padding-bottom: calc(var(--spacing-unit) * 2);
margin-bottom: calc(var(--spacing-unit) * 2);
}
+
+/* Footer Styles */
+.app-footer {
+ background-color: var(--accent-color);
+ border-top: 1px solid var(--border-color);
+ padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 3);
+ text-align: center;
+ font-size: 0.875rem;
+ color: var(--text-color-light);
+ flex-shrink: 0; /* Prevent footer from shrinking */
+}
+
+.footer-nav {
+ margin-bottom: var(--spacing-unit);
+}
+
+.footer-links {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: calc(var(--spacing-unit) * 2);
+}
+
+.footer-links li {
+ display: inline;
+}
+
+.footer-links a {
+ color: var(--primary-color);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.footer-links a:hover {
+ color: var(--secondary-color);
+ text-decoration: underline;
+}
+
+.footer-text {
+ margin: 0;
+}
diff --git a/static/js/components/cookie-consent.js b/static/js/components/cookie-consent.js
new file mode 100644
index 0000000..0f436d6
--- /dev/null
+++ b/static/js/components/cookie-consent.js
@@ -0,0 +1,146 @@
+// static/js/components/cookie-consent.js
+import app from '../app.js';
+
+const COOKIE_CONSENT_KEY = 'rbox_cookie_consent';
+
+export class CookieConsent extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this.hasConsented = this.checkConsent();
+ }
+
+ connectedCallback() {
+ if (!this.hasConsented) {
+ this.render();
+ }
+ }
+
+ checkConsent() {
+ const consent = localStorage.getItem(COOKIE_CONSENT_KEY);
+ if (consent) {
+ // In a real application, you'd parse this and apply preferences
+ // For now, just checking if it exists
+ return true;
+ }
+ return false;
+ }
+
+ setConsent(status) {
+ // In a real application, 'status' would be a detailed object
+ // For now, a simple string 'accepted' or 'declined'
+ localStorage.setItem(COOKIE_CONSENT_KEY, status);
+ this.hasConsented = true;
+ this.remove(); // Remove the banner after consent
+ app.getLogger().info(`Cookie consent: ${status}`);
+ // Trigger an event for other parts of the app to react to consent change
+ document.dispatchEvent(new CustomEvent('cookie-consent-changed', { detail: { status } }));
+ }
+
+ render() {
+ this.shadowRoot.innerHTML = `
+
+
+
+ We use cookies to ensure you get the best experience on our website. For more details, please read our
+ Cookie Policy.
+
+
+
+
+
+
+
+ `;
+
+ this.shadowRoot.querySelector('.consent-button.accept').addEventListener('click', () => this.setConsent('accepted'));
+ this.shadowRoot.querySelector('.consent-button.decline').addEventListener('click', () => this.setConsent('declined'));
+ this.shadowRoot.querySelector('.consent-button.customize').addEventListener('click', () => {
+ // For now, customize acts like accept. In a real app, this would open a modal.
+ app.getLogger().info('Customize cookie consent clicked. (Placeholder: acting as accept)');
+ this.setConsent('accepted');
+ });
+ }
+}
+
+customElements.define('cookie-consent', CookieConsent);
diff --git a/static/js/components/login-view.js b/static/js/components/login-view.js
index 1a3e0fb..27613a2 100644
--- a/static/js/components/login-view.js
+++ b/static/js/components/login-view.js
@@ -83,12 +83,12 @@ export class LoginView extends HTMLElement {
errorDiv.textContent = '';
try {
- logger.info('Login attempt started', { username });
+ logger.info('Login attempt started', { action: 'USER_LOGIN_ATTEMPT', username });
await api.login(username, password);
- logger.info('Login successful, dispatching auth-success event');
+ logger.info('Login successful', { action: 'USER_LOGIN_SUCCESS', username });
this.dispatchEvent(new CustomEvent('auth-success'));
} catch (error) {
- logger.error('Login failed', { username, error: error.message });
+ logger.error('Login failed', { action: 'USER_LOGIN_FAILURE', username, error: error.message });
errorDiv.textContent = error.message;
errorDiv.style.display = 'block';
}
@@ -107,12 +107,12 @@ export class LoginView extends HTMLElement {
errorDiv.textContent = '';
try {
- logger.info('Registration attempt started', { username, email });
+ logger.info('Registration attempt started', { action: 'USER_REGISTER_ATTEMPT', username, email });
await api.register(username, email, password);
- logger.info('Registration successful, dispatching auth-success event');
+ logger.info('Registration successful', { action: 'USER_REGISTER_SUCCESS', username, email });
this.dispatchEvent(new CustomEvent('auth-success'));
} catch (error) {
- logger.error('Registration failed', { username, email, error: error.message });
+ logger.error('Registration failed', { action: 'USER_REGISTER_FAILURE', username, email, error: error.message });
errorDiv.textContent = error.message;
errorDiv.style.display = 'block';
}
diff --git a/static/js/components/rbox-app.js b/static/js/components/rbox-app.js
index 86805e6..9520ae7 100644
--- a/static/js/components/rbox-app.js
+++ b/static/js/components/rbox-app.js
@@ -14,6 +14,8 @@ import './shared-items.js';
import './billing-dashboard.js';
import './admin-billing.js';
import './code-editor-view.js';
+import './cookie-consent.js';
+import './user-settings.js'; // Import the new user settings component
import { shortcuts } from '../shortcuts.js';
const api = app.getAPI();
@@ -126,6 +128,7 @@ export class RBoxApp extends HTMLElement {
Shared Items
Deleted Files
Billing
+ User Settings
${this.user && this.user.is_superuser ? `Admin Dashboard` : ''}
${this.user && this.user.is_superuser ? `Admin Billing` : ''}
@@ -145,7 +148,24 @@ export class RBoxApp extends HTMLElement {
+
+
+
`;
this.initializeNavigation();
@@ -358,6 +378,7 @@ export class RBoxApp extends HTMLElement {
attachListeners() {
this.querySelector('#logout-btn')?.addEventListener('click', () => {
+ logger.info('User logout initiated', { action: 'USER_LOGOUT' });
api.logout();
});
@@ -639,6 +660,10 @@ export class RBoxApp extends HTMLElement {
mainContent.innerHTML = '';
this.attachListeners();
break;
+ case 'user-settings':
+ mainContent.innerHTML = '';
+ this.attachListeners();
+ break;
case 'admin-billing':
mainContent.innerHTML = '';
this.attachListeners();
diff --git a/static/js/components/user-settings.js b/static/js/components/user-settings.js
new file mode 100644
index 0000000..76c599f
--- /dev/null
+++ b/static/js/components/user-settings.js
@@ -0,0 +1,117 @@
+// static/js/components/user-settings.js
+import app from '../app.js';
+
+const api = app.getAPI();
+const logger = app.getLogger();
+
+export class UserSettings extends HTMLElement {
+ constructor() {
+ super();
+ this.boundHandleClick = this.handleClick.bind(this);
+ }
+
+ connectedCallback() {
+ this.render();
+ this.addEventListener('click', this.boundHandleClick);
+ }
+
+ disconnectedCallback() {
+ this.removeEventListener('click', this.boundHandleClick);
+ }
+
+ render() {
+ this.innerHTML = `
+
+
User Settings
+
+
+
Data Management
+
You can export a copy of your personal data or delete your account.
+
+
+
+
+
+
+
+ `;
+ }
+
+ async handleClick(event) {
+ if (event.target.id === 'exportDataBtn') {
+ await this.exportUserData();
+ } else if (event.target.id === 'deleteAccountBtn') {
+ await this.deleteAccount();
+ }
+ }
+
+ async exportUserData() {
+ try {
+ logger.info('Initiating data export...', { action: 'USER_DATA_EXPORT_ATTEMPT' });
+ const response = await fetch('/api/users/me/export', {
+ headers: {
+ 'Authorization': `Bearer ${api.getToken()}`,
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ const filename = `rbox_user_data_${new Date().toISOString().slice(0,10)}.json`;
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ logger.info('User data exported successfully.', { action: 'USER_DATA_EXPORT_SUCCESS' });
+ document.dispatchEvent(new CustomEvent('show-toast', {
+ detail: { message: 'Your data has been exported successfully!', type: 'success' }
+ }));
+ } catch (error) {
+ logger.error('Failed to export user data:', { action: 'USER_DATA_EXPORT_FAILURE', error: error.message });
+ document.dispatchEvent(new CustomEvent('show-toast', {
+ detail: { message: `Failed to export data: ${error.message}`, type: 'error' }
+ }));
+ }
+ }
+
+ async deleteAccount() {
+ if (!confirm('Are you absolutely sure you want to delete your account? This action cannot be undone and all your data will be permanently lost.')) {
+ return;
+ }
+
+ try {
+ logger.warn('Initiating account deletion...', { action: 'USER_ACCOUNT_DELETE_ATTEMPT' });
+ const response = await fetch('/api/users/me', {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${api.getToken()}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ logger.info('Account deleted successfully. Logging out...', { action: 'USER_ACCOUNT_DELETE_SUCCESS' });
+ api.logout(); // Clear token and redirect to login
+ document.dispatchEvent(new CustomEvent('show-toast', {
+ detail: { message: 'Your account has been successfully deleted.', type: 'success' }
+ }));
+ } catch (error) {
+ logger.error('Failed to delete account:', { action: 'USER_ACCOUNT_DELETE_FAILURE', error: error.message });
+ document.dispatchEvent(new CustomEvent('show-toast', {
+ detail: { message: `Failed to delete account: ${error.message}`, type: 'error' }
+ }));
+ }
+ }
+}
+
+customElements.define('user-settings', UserSettings);
diff --git a/tests/e2e/test_admin.py b/tests/e2e/test_admin.py
new file mode 100644
index 0000000..b7ff2fb
--- /dev/null
+++ b/tests/e2e/test_admin.py
@@ -0,0 +1,181 @@
+import pytest
+import asyncio
+from playwright.async_api import expect
+
+@pytest.mark.asyncio
+class TestAdmin:
+
+ async def test_01_admin_login(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Admin should see admin nav links
+ await expect(page.locator('a.nav-link[data-view="admin"]')).to_be_visible()
+ await expect(page.locator('a.nav-link[data-view="admin-billing"]')).to_be_visible()
+
+ async def test_02_navigate_to_admin_dashboard(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('admin-dashboard')).to_be_visible()
+
+ async def test_03_view_user_management(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin"]')
+ await page.wait_for_timeout(1000)
+
+ # Should show user list
+ await expect(page.locator('.user-list')).to_be_visible()
+ await expect(page.locator('text=e2etestuser')).to_be_visible()
+
+ async def test_04_view_system_statistics(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin"]')
+ await page.wait_for_timeout(1000)
+
+ # Should show system stats
+ await expect(page.locator('.system-stats')).to_be_visible()
+ await expect(page.locator('.stat-item')).to_be_visible()
+
+ async def test_05_manage_user_permissions(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin"]')
+ await page.wait_for_timeout(1000)
+
+ # Click on user to manage
+ await page.click('.user-item:has-text("e2etestuser")')
+ await page.wait_for_timeout(500)
+
+ # Should show user details modal
+ await expect(page.locator('.user-details-modal')).to_be_visible()
+
+ # Change permissions
+ await page.select_option('select[name="role"]', 'moderator')
+ await page.click('button:has-text("Save")')
+ await page.wait_for_timeout(1000)
+
+ async def test_06_view_storage_usage(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin"]')
+ await page.wait_for_timeout(1000)
+
+ # Should show storage usage
+ await expect(page.locator('.storage-usage')).to_be_visible()
+
+ async def test_07_navigate_to_admin_billing(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin-billing"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('admin-billing')).to_be_visible()
+
+ async def test_08_view_billing_statistics(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin-billing"]')
+ await page.wait_for_timeout(1000)
+
+ # Should show billing stats
+ await expect(page.locator('.billing-stats')).to_be_visible()
+
+ async def test_09_manage_pricing(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin-billing"]')
+ await page.wait_for_timeout(1000)
+
+ # Click pricing management
+ await page.click('button:has-text("Manage Pricing")')
+ await page.wait_for_timeout(500)
+
+ # Should show pricing form
+ await expect(page.locator('.pricing-form')).to_be_visible()
+
+ # Update a price
+ await page.fill('input[name="storage_per_gb_month"]', '0.005')
+ await page.click('button:has-text("Update")')
+ await page.wait_for_timeout(1000)
+
+ async def test_10_generate_invoice(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'adminuser')
+ await page.fill('#login-form input[name="password"]', 'adminpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="admin-billing"]')
+ await page.wait_for_timeout(1000)
+
+ # Click generate invoice
+ await page.click('button:has-text("Generate Invoice")')
+ await page.wait_for_timeout(500)
+
+ # Select user
+ await page.select_option('select[name="user"]', 'e2etestuser')
+ await page.click('button:has-text("Generate")')
+ await page.wait_for_timeout(1000)
+
+ # Should show success message
+ await expect(page.locator('text=Invoice generated')).to_be_visible()
\ No newline at end of file
diff --git a/tests/e2e/test_auth_flow.py b/tests/e2e/test_auth_flow.py
new file mode 100644
index 0000000..072dfe5
--- /dev/null
+++ b/tests/e2e/test_auth_flow.py
@@ -0,0 +1,170 @@
+import pytest
+import asyncio
+from playwright.async_api import expect
+
+@pytest.mark.asyncio
+class TestAuthFlow:
+
+ async def test_01_user_registration(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ # Click Sign Up
+ await page.click('text=Sign Up')
+ await page.wait_for_timeout(500)
+
+ # Fill registration form
+ await page.fill('#register-form input[name="username"]', 'e2etestuser')
+ await page.fill('#register-form input[name="email"]', 'e2etestuser@example.com')
+ await page.fill('#register-form input[name="password"]', 'testpassword123')
+
+ # Submit registration
+ await page.click('#register-form button[type="submit"]')
+
+ await page.wait_for_timeout(2000)
+ await expect(page.locator('text=My Files')).to_be_visible()
+
+ async def test_02_user_login(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ # Unregister service worker to avoid interference
+ await page.evaluate("""
+ navigator.serviceWorker.getRegistrations().then(function(registrations) {
+ for(let registration of registrations) {
+ registration.unregister();
+ }
+ });
+ """)
+
+ await page.reload()
+ await page.wait_for_load_state("networkidle")
+
+ # Fill login form
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+
+ # Submit login
+ await page.click('#login-form button[type="submit"]')
+
+ await page.wait_for_timeout(2000)
+ await expect(page.locator('text=My Files')).to_be_visible()
+
+ async def test_03_navigate_to_files_view(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Files view should be active by default
+ await expect(page.locator('a.nav-link.active[data-view="files"]')).to_be_visible()
+ await expect(page.locator('file-list')).to_be_visible()
+
+ async def test_04_navigate_to_photos_view(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('a.nav-link.active[data-view="photos"]')).to_be_visible()
+ await expect(page.locator('photo-gallery')).to_be_visible()
+
+ async def test_05_navigate_to_shared_view(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="shared"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('a.nav-link.active[data-view="shared"]')).to_be_visible()
+ await expect(page.locator('shared-items')).to_be_visible()
+
+ async def test_06_navigate_to_starred_view(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="starred"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('a.nav-link.active[data-view="starred"]')).to_be_visible()
+ await expect(page.locator('starred-items')).to_be_visible()
+
+ async def test_07_navigate_to_recent_view(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="recent"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('a.nav-link.active[data-view="recent"]')).to_be_visible()
+ await expect(page.locator('recent-files')).to_be_visible()
+
+ async def test_08_navigate_to_deleted_view(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="deleted"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('a.nav-link.active[data-view="deleted"]')).to_be_visible()
+ await expect(page.locator('deleted-files')).to_be_visible()
+
+ async def test_09_navigate_to_billing_view(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="billing"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('a.nav-link.active[data-view="billing"]')).to_be_visible()
+ await expect(page.locator('billing-dashboard')).to_be_visible()
+
+ async def test_10_user_logout(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Click logout
+ await page.click('#logout-btn')
+ await page.wait_for_timeout(1000)
+
+ # Should be back to login
+ await expect(page.locator('#login-form')).to_be_visible()
\ No newline at end of file
diff --git a/tests/e2e/test_file_management.py b/tests/e2e/test_file_management.py
new file mode 100644
index 0000000..b68a863
--- /dev/null
+++ b/tests/e2e/test_file_management.py
@@ -0,0 +1,222 @@
+import pytest
+import asyncio
+from playwright.async_api import expect
+
+@pytest.mark.asyncio
+class TestFileManagement:
+
+ async def test_01_upload_file(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Upload a file
+ await page.set_input_files('input[type="file"]', {
+ 'name': 'test-file.txt',
+ 'mimeType': 'text/plain',
+ 'buffer': b'This is a test file for e2e testing.'
+ })
+
+ await page.click('button:has-text("Upload")')
+ await page.wait_for_timeout(2000)
+
+ await expect(page.locator('text=test-file.txt')).to_be_visible()
+
+ async def test_02_create_folder(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Click create folder button
+ await page.click('button:has-text("New Folder")')
+ await page.wait_for_timeout(500)
+
+ # Fill folder name
+ await page.fill('input[placeholder="Folder name"]', 'Test Folder')
+ await page.click('button:has-text("Create")')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('text=Test Folder')).to_be_visible()
+
+ async def test_03_navigate_into_folder(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Click on the folder
+ await page.click('text=Test Folder')
+ await page.wait_for_timeout(1000)
+
+ # Should be in the folder, breadcrumb should show it
+ await expect(page.locator('.breadcrumb')).to_contain_text('Test Folder')
+
+ async def test_04_upload_file_in_folder(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Navigate to folder
+ await page.click('text=Test Folder')
+ await page.wait_for_timeout(1000)
+
+ # Upload file in folder
+ await page.set_input_files('input[type="file"]', {
+ 'name': 'folder-file.txt',
+ 'mimeType': 'text/plain',
+ 'buffer': b'This file is in a folder.'
+ })
+
+ await page.click('button:has-text("Upload")')
+ await page.wait_for_timeout(2000)
+
+ await expect(page.locator('text=folder-file.txt')).to_be_visible()
+
+ async def test_05_navigate_back_from_folder(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Navigate to folder
+ await page.click('text=Test Folder')
+ await page.wait_for_timeout(1000)
+
+ # Click breadcrumb to go back
+ await page.click('.breadcrumb a:first-child')
+ await page.wait_for_timeout(1000)
+
+ # Should be back in root
+ await expect(page.locator('text=Test Folder')).to_be_visible()
+ await expect(page.locator('text=test-file.txt')).to_be_visible()
+
+ async def test_06_select_and_delete_file(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Select file
+ await page.click('.file-item:has-text("test-file.txt") .file-checkbox')
+ await page.wait_for_timeout(500)
+
+ # Click delete
+ await page.click('button:has-text("Delete")')
+ await page.wait_for_timeout(500)
+
+ # Confirm delete
+ await page.click('button:has-text("Confirm")')
+ await page.wait_for_timeout(1000)
+
+ # File should be gone
+ await expect(page.locator('text=test-file.txt')).not_to_be_visible()
+
+ async def test_07_restore_deleted_file(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Go to deleted files
+ await page.click('a.nav-link[data-view="deleted"]')
+ await page.wait_for_timeout(1000)
+
+ # Select deleted file
+ await page.click('.file-item:has-text("test-file.txt") .file-checkbox')
+ await page.wait_for_timeout(500)
+
+ # Click restore
+ await page.click('button:has-text("Restore")')
+ await page.wait_for_timeout(1000)
+
+ # Go back to files
+ await page.click('a.nav-link[data-view="files"]')
+ await page.wait_for_timeout(1000)
+
+ # File should be back
+ await expect(page.locator('text=test-file.txt')).to_be_visible()
+
+ async def test_08_rename_folder(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Right-click on folder or use context menu
+ await page.click('.folder-item:has-text("Test Folder")', button='right')
+ await page.wait_for_timeout(500)
+
+ # Click rename
+ await page.click('text=Rename')
+ await page.wait_for_timeout(500)
+
+ # Fill new name
+ await page.fill('input[placeholder="New name"]', 'Renamed Folder')
+ await page.click('button:has-text("Rename")')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('text=Renamed Folder')).to_be_visible()
+ await expect(page.locator('text=Test Folder')).not_to_be_visible()
+
+ async def test_09_star_file(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Click star on file
+ await page.click('.file-item:has-text("test-file.txt") .star-btn')
+ await page.wait_for_timeout(1000)
+
+ # Go to starred
+ await page.click('a.nav-link[data-view="starred"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('text=test-file.txt')).to_be_visible()
+
+ async def test_10_search_files(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Type in search
+ await page.fill('#search-input', 'test-file')
+ await page.wait_for_timeout(1000)
+
+ # Should show search results
+ await expect(page.locator('.search-results')).to_be_visible()
+ await expect(page.locator('text=test-file.txt')).to_be_visible()
\ No newline at end of file
diff --git a/tests/e2e/test_photo_gallery.py b/tests/e2e/test_photo_gallery.py
new file mode 100644
index 0000000..3bad93b
--- /dev/null
+++ b/tests/e2e/test_photo_gallery.py
@@ -0,0 +1,227 @@
+import pytest
+import asyncio
+from playwright.async_api import expect
+
+@pytest.mark.asyncio
+class TestPhotoGallery:
+
+ async def test_01_upload_image_file(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Upload an image file
+ await page.set_input_files('input[type="file"]', {
+ 'name': 'test-image.jpg',
+ 'mimeType': 'image/jpeg',
+ 'buffer': b'fake image data' # In real test, use actual image bytes
+ })
+
+ await page.click('button:has-text("Upload")')
+ await page.wait_for_timeout(2000)
+
+ await expect(page.locator('text=test-image.jpg')).to_be_visible()
+
+ async def test_02_navigate_to_photo_gallery(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('photo-gallery')).to_be_visible()
+
+ async def test_03_view_image_in_gallery(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ # Should show uploaded image
+ await expect(page.locator('.photo-item')).to_be_visible()
+
+ async def test_04_click_on_photo_to_preview(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ # Click on photo
+ await page.click('.photo-item img')
+ await page.wait_for_timeout(1000)
+
+ # Should open preview
+ await expect(page.locator('file-preview')).to_be_visible()
+
+ async def test_05_close_photo_preview(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ # Click on photo
+ await page.click('.photo-item img')
+ await page.wait_for_timeout(1000)
+
+ # Close preview
+ await page.click('.preview-close-btn')
+ await page.wait_for_timeout(500)
+
+ # Preview should be closed
+ await expect(page.locator('file-preview')).not_to_be_visible()
+
+ async def test_06_upload_multiple_images(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Upload multiple images
+ await page.set_input_files('input[type="file"]', [
+ {
+ 'name': 'image1.jpg',
+ 'mimeType': 'image/jpeg',
+ 'buffer': b'fake image 1'
+ },
+ {
+ 'name': 'image2.png',
+ 'mimeType': 'image/png',
+ 'buffer': b'fake image 2'
+ }
+ ])
+
+ await page.click('button:has-text("Upload")')
+ await page.wait_for_timeout(2000)
+
+ # Go to gallery
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ # Should show multiple photos
+ photo_count = await page.locator('.photo-item').count()
+ assert photo_count >= 2
+
+ async def test_07_filter_photos_by_date(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ # Click date filter
+ await page.click('.date-filter-btn')
+ await page.wait_for_timeout(500)
+
+ # Select today
+ await page.click('text=Today')
+ await page.wait_for_timeout(1000)
+
+ # Should filter photos
+ await expect(page.locator('.photo-item')).to_be_visible()
+
+ async def test_08_search_photos(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ # Type in gallery search
+ await page.fill('.gallery-search-input', 'image1')
+ await page.wait_for_timeout(1000)
+
+ # Should show filtered results
+ await expect(page.locator('text=image1.jpg')).to_be_visible()
+ await expect(page.locator('text=image2.png')).not_to_be_visible()
+
+ async def test_09_create_album(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ # Click create album
+ await page.click('button:has-text("Create Album")')
+ await page.wait_for_timeout(500)
+
+ # Fill album name
+ await page.fill('input[name="album-name"]', 'Test Album')
+ await page.click('button:has-text("Create")')
+ await page.wait_for_timeout(1000)
+
+ # Should show album
+ await expect(page.locator('text=Test Album')).to_be_visible()
+
+ async def test_10_add_photos_to_album(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ await page.click('a.nav-link[data-view="photos"]')
+ await page.wait_for_timeout(1000)
+
+ # Select photos
+ await page.click('.photo-item .photo-checkbox', { force: True })
+ await page.wait_for_timeout(500)
+
+ # Click add to album
+ await page.click('button:has-text("Add to Album")')
+ await page.wait_for_timeout(500)
+
+ # Select album
+ await page.click('text=Test Album')
+ await page.click('button:has-text("Add")')
+ await page.wait_for_timeout(1000)
+
+ # Should show success
+ await expect(page.locator('text=Photos added to album')).to_be_visible()
\ No newline at end of file
diff --git a/tests/e2e/test_sharing.py b/tests/e2e/test_sharing.py
new file mode 100644
index 0000000..cde8e84
--- /dev/null
+++ b/tests/e2e/test_sharing.py
@@ -0,0 +1,207 @@
+import pytest
+import asyncio
+from playwright.async_api import expect
+
+@pytest.mark.asyncio
+class TestSharing:
+
+ async def test_01_share_file_with_user(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Select file
+ await page.click('.file-item:has-text("test-file.txt") .file-checkbox')
+ await page.wait_for_timeout(500)
+
+ # Click share
+ await page.click('button:has-text("Share")')
+ await page.wait_for_timeout(500)
+
+ # Share modal should appear
+ await expect(page.locator('share-modal')).to_be_visible()
+
+ # Fill share form
+ await page.fill('input[placeholder="Username to share with"]', 'billingtest')
+ await page.select_option('select[name="permission"]', 'read')
+ await page.click('button:has-text("Share")')
+ await page.wait_for_timeout(1000)
+
+ # Modal should close
+ await expect(page.locator('share-modal')).not_to_be_visible()
+
+ async def test_02_view_shared_items_as_recipient(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'billingtest')
+ await page.fill('#login-form input[name="password"]', 'password123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Go to shared items
+ await page.click('a.nav-link[data-view="shared"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('text=test-file.txt')).to_be_visible()
+
+ async def test_03_download_shared_file(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'billingtest')
+ await page.fill('#login-form input[name="password"]', 'password123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Go to shared items
+ await page.click('a.nav-link[data-view="shared"]')
+ await page.wait_for_timeout(1000)
+
+ # Click download on shared file
+ await page.click('.file-item:has-text("test-file.txt") .download-btn')
+ await page.wait_for_timeout(1000)
+
+ # Should trigger download (can't easily test actual download in e2e)
+
+ async def test_04_create_public_link(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Select file
+ await page.click('.file-item:has-text("test-file.txt") .file-checkbox')
+ await page.wait_for_timeout(500)
+
+ # Click share
+ await page.click('button:has-text("Share")')
+ await page.wait_for_timeout(500)
+
+ # Click create public link
+ await page.click('button:has-text("Create Public Link")')
+ await page.wait_for_timeout(1000)
+
+ # Should show link
+ await expect(page.locator('.public-link')).to_be_visible()
+
+ async def test_05_access_public_link(self, page, base_url):
+ # First get the public link from previous test
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Get the public link
+ await page.click('.file-item:has-text("test-file.txt") .file-checkbox')
+ await page.click('button:has-text("Share")')
+ await page.wait_for_timeout(500)
+
+ link_element = page.locator('.public-link input')
+ public_url = await link_element.get_attribute('value')
+
+ # Logout
+ await page.click('#logout-btn')
+ await page.wait_for_timeout(1000)
+
+ # Access public link
+ await page.goto(public_url)
+ await page.wait_for_load_state("networkidle")
+
+ # Should be able to view/download the file
+ await expect(page.locator('text=test-file.txt')).to_be_visible()
+
+ async def test_06_revoke_share(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Go to shared items view (as owner)
+ await page.click('a.nav-link[data-view="shared"]')
+ await page.wait_for_timeout(1000)
+
+ # Find the share and revoke
+ await page.click('.share-item .revoke-btn')
+ await page.wait_for_timeout(500)
+
+ # Confirm revoke
+ await page.click('button:has-text("Confirm")')
+ await page.wait_for_timeout(1000)
+
+ # Share should be gone
+ await expect(page.locator('.share-item')).not_to_be_visible()
+
+ async def test_07_share_folder(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'e2etestuser')
+ await page.fill('#login-form input[name="password"]', 'testpassword123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Select folder
+ await page.click('.folder-item:has-text("Renamed Folder") .folder-checkbox')
+ await page.wait_for_timeout(500)
+
+ # Click share
+ await page.click('button:has-text("Share")')
+ await page.wait_for_timeout(500)
+
+ # Share modal should appear
+ await expect(page.locator('share-modal')).to_be_visible()
+
+ # Fill share form
+ await page.fill('input[placeholder="Username to share with"]', 'billingtest')
+ await page.select_option('select[name="permission"]', 'read')
+ await page.click('button:has-text("Share")')
+ await page.wait_for_timeout(1000)
+
+ async def test_08_view_shared_folder_as_recipient(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'billingtest')
+ await page.fill('#login-form input[name="password"]', 'password123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Go to shared items
+ await page.click('a.nav-link[data-view="shared"]')
+ await page.wait_for_timeout(1000)
+
+ await expect(page.locator('text=Renamed Folder')).to_be_visible()
+
+ async def test_09_navigate_into_shared_folder(self, page, base_url):
+ await page.goto(f"{base_url}/")
+ await page.wait_for_load_state("networkidle")
+
+ await page.fill('#login-form input[name="username"]', 'billingtest')
+ await page.fill('#login-form input[name="password"]', 'password123')
+ await page.click('#login-form button[type="submit"]')
+ await page.wait_for_timeout(2000)
+
+ # Go to shared items
+ await page.click('a.nav-link[data-view="shared"]')
+ await page.wait_for_timeout(1000)
+
+ # Click on shared folder
+ await page.click('text=Renamed Folder')
+ await page.wait_for_timeout(1000)
+
+ # Should see the file inside
+ await expect(page.locator('text=folder-file.txt')).to_be_visible()
\ No newline at end of file