This commit is contained in:
retoor 2025-11-13 11:47:50 +01:00
parent b8d30af69e
commit b23fd25337
11 changed files with 1386 additions and 7 deletions

View File

@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ..auth import get_current_user 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( router = APIRouter(
prefix="/users", prefix="/users",
@ -10,3 +11,42 @@ router = APIRouter(
@router.get("/me", response_model=User_Pydantic) @router.get("/me", response_model=User_Pydantic)
async def read_users_me(current_user: User = Depends(get_current_user)): async def read_users_me(current_user: User = Depends(get_current_user)):
return await User_Pydantic.from_tortoise_orm(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 {}

View File

@ -998,3 +998,47 @@ body.dark-mode {
padding-bottom: calc(var(--spacing-unit) * 2); padding-bottom: calc(var(--spacing-unit) * 2);
margin-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;
}

View File

@ -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 = `
<style>
:host {
display: block;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--accent-color, #333);
color: var(--text-color-light, #eee);
padding: 15px 20px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
z-index: 10000;
font-family: var(--font-family, sans-serif);
font-size: 0.9rem;
line-height: 1.4;
}
.consent-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 15px;
}
.consent-message {
flex: 1;
min-width: 250px;
color: var(--text-color, #333);
}
.consent-message a {
color: var(--primary-color, #007bff);
text-decoration: underline;
}
.consent-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.consent-button {
background-color: var(--primary-color, #007bff);
color: var(--accent-color, #fff);
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s ease;
}
.consent-button:hover {
background-color: var(--secondary-color, #0056b3);
}
.consent-button.decline {
background-color: #6c757d;
}
.consent-button.decline:hover {
background-color: #5a6268;
}
.consent-button.customize {
background-color: transparent;
border: 1px solid var(--primary-color, #007bff);
color: var(--primary-color, #007bff);
}
.consent-button.customize:hover {
background-color: var(--primary-color, #007bff);
color: var(--accent-color, #fff);
}
@media (max-width: 768px) {
.consent-container {
flex-direction: column;
align-items: flex-start;
}
.consent-buttons {
width: 100%;
justify-content: stretch;
}
.consent-button {
flex: 1;
}
}
</style>
<div class="consent-container">
<p class="consent-message">
We use cookies to ensure you get the best experience on our website. For more details, please read our
<a href="/static/legal/cookie_policy.md" target="_blank" rel="noopener noreferrer">Cookie Policy</a>.
</p>
<div class="consent-buttons">
<button class="consent-button accept">Accept All</button>
<button class="consent-button decline">Decline All</button>
<button class="consent-button customize">Customize</button>
</div>
</div>
`;
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);

View File

@ -83,12 +83,12 @@ export class LoginView extends HTMLElement {
errorDiv.textContent = ''; errorDiv.textContent = '';
try { try {
logger.info('Login attempt started', { username }); logger.info('Login attempt started', { action: 'USER_LOGIN_ATTEMPT', username });
await api.login(username, password); 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')); this.dispatchEvent(new CustomEvent('auth-success'));
} catch (error) { } 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.textContent = error.message;
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
} }
@ -107,12 +107,12 @@ export class LoginView extends HTMLElement {
errorDiv.textContent = ''; errorDiv.textContent = '';
try { 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); 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')); this.dispatchEvent(new CustomEvent('auth-success'));
} catch (error) { } 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.textContent = error.message;
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
} }

View File

@ -14,6 +14,8 @@ import './shared-items.js';
import './billing-dashboard.js'; import './billing-dashboard.js';
import './admin-billing.js'; import './admin-billing.js';
import './code-editor-view.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'; import { shortcuts } from '../shortcuts.js';
const api = app.getAPI(); const api = app.getAPI();
@ -126,6 +128,7 @@ export class RBoxApp extends HTMLElement {
<li><a href="#" class="nav-link" data-view="shared">Shared Items</a></li> <li><a href="#" class="nav-link" data-view="shared">Shared Items</a></li>
<li><a href="#" class="nav-link" data-view="deleted">Deleted Files</a></li> <li><a href="#" class="nav-link" data-view="deleted">Deleted Files</a></li>
<li><a href="#" class="nav-link" data-view="billing">Billing</a></li> <li><a href="#" class="nav-link" data-view="billing">Billing</a></li>
<li><a href="#" class="nav-link" data-view="user-settings">User Settings</a></li>
${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin">Admin Dashboard</a></li>` : ''} ${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin">Admin Dashboard</a></li>` : ''}
${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin-billing">Admin Billing</a></li>` : ''} ${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin-billing">Admin Billing</a></li>` : ''}
</ul> </ul>
@ -145,7 +148,24 @@ export class RBoxApp extends HTMLElement {
</div> </div>
<share-modal></share-modal> <share-modal></share-modal>
<footer class="app-footer">
<nav class="footer-nav">
<ul class="footer-links">
<li><a href="/static/legal/privacy_policy.md" target="_blank" rel="noopener noreferrer">Privacy Policy</a></li>
<li><a href="/static/legal/data_processing_agreement.md" target="_blank" rel="noopener noreferrer">Data Processing Agreement</a></li>
<li><a href="/static/legal/terms_of_service.md" target="_blank" rel="noopener noreferrer">Terms of Service</a></li>
<li><a href="/static/legal/cookie_policy.md" target="_blank" rel="noopener noreferrer">Cookie Policy</a></li>
<li><a href="/static/legal/security_policy.md" target="_blank" rel="noopener noreferrer">Security Policy</a></li>
<li><a href="/static/legal/compliance_statement.md" target="_blank" rel="noopener noreferrer">Compliance Statement</a></li>
<li><a href="/static/legal/data_portability_deletion_policy.md" target="_blank" rel="noopener noreferrer">Data Portability & Deletion</a></li>
<li><a href="/static/legal/contact_complaint_mechanism.md" target="_blank" rel="noopener noreferrer">Contact & Complaints</a></li>
</ul>
</nav>
<p class="footer-text">&copy; ${new Date().getFullYear()} RBox Cloud Storage. All rights reserved.</p>
</footer>
</div> </div>
<cookie-consent></cookie-consent>
`; `;
this.initializeNavigation(); this.initializeNavigation();
@ -358,6 +378,7 @@ export class RBoxApp extends HTMLElement {
attachListeners() { attachListeners() {
this.querySelector('#logout-btn')?.addEventListener('click', () => { this.querySelector('#logout-btn')?.addEventListener('click', () => {
logger.info('User logout initiated', { action: 'USER_LOGOUT' });
api.logout(); api.logout();
}); });
@ -639,6 +660,10 @@ export class RBoxApp extends HTMLElement {
mainContent.innerHTML = '<billing-dashboard></billing-dashboard>'; mainContent.innerHTML = '<billing-dashboard></billing-dashboard>';
this.attachListeners(); this.attachListeners();
break; break;
case 'user-settings':
mainContent.innerHTML = '<user-settings></user-settings>';
this.attachListeners();
break;
case 'admin-billing': case 'admin-billing':
mainContent.innerHTML = '<admin-billing></admin-billing>'; mainContent.innerHTML = '<admin-billing></admin-billing>';
this.attachListeners(); this.attachListeners();

View File

@ -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 = `
<div class="user-settings-container">
<h2>User Settings</h2>
<div class="settings-section">
<h3>Data Management</h3>
<p>You can export a copy of your personal data or delete your account.</p>
<button id="exportDataBtn" class="button button-primary">Export My Data</button>
<button id="deleteAccountBtn" class="button button-danger">Delete My Account</button>
</div>
<!-- Add more settings sections here later -->
</div>
`;
}
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);

181
tests/e2e/test_admin.py Normal file
View File

@ -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()

170
tests/e2e/test_auth_flow.py Normal file
View File

@ -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()

View File

@ -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()

View File

@ -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()

207
tests/e2e/test_sharing.py Normal file
View File

@ -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()