Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,23 @@ jobs:

- name: Run tests
run: cargo test --verbose

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install Playwright Dependencies
run: |
cd tests/playwright
npm ci

- name: Install Playwright Browsers
run: |
cd tests/playwright
npx playwright install --with-deps

- name: Run Playwright Tests
run: |
cd tests/playwright
npx playwright test
4 changes: 4 additions & 0 deletions tests/playwright/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
test-results/
playwright-report/
test-data/
96 changes: 96 additions & 0 deletions tests/playwright/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions tests/playwright/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "lanshare-e2e",
"version": "1.0.0",
"private": true,
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.41.0",
"@types/node": "^20.11.0"
}
}
31 changes: 31 additions & 0 deletions tests/playwright/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineConfig, devices } from '@playwright/test';
import path from 'path';

export default defineConfig({
testDir: './specs',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'line',
use: {
baseURL: 'http://127.0.0.1:9000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
// Run cargo run from the repo root
// We use a specific test root and token
command: 'cargo run -- --bind 127.0.0.1:9000 --root ./tests/playwright/test-data --token secret-token --max-file-bytes 100 --overwrite deny',
cwd: '../../', // Go up two levels from tests/playwright to repo root
url: 'http://127.0.0.1:9000',
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
},
});
109 changes: 109 additions & 0 deletions tests/playwright/specs/lanshare.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';

const TEST_DATA_DIR = path.join(__dirname, '../test-data');

// Clean up test data before and after tests
test.beforeEach(async () => {
// We can't easily clean the directory if the server is holding locks (Windows),
// but on Linux/Mac it's usually fine. The server creates the directory.
// We'll rely on the server creating it, but we can try to empty it if it exists.
// Note: The server is started by the global webServer config.

// Create a dummy file to see in the list
if (!fs.existsSync(TEST_DATA_DIR)) {
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
fs.writeFileSync(path.join(TEST_DATA_DIR, 'hello.txt'), 'Hello world');
});

test.describe('Lanshare Client UI', () => {

test('lists files in the directory', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle('File Server');

// Check for the file we created in beforeEach
const fileLink = page.getByRole('link', { name: 'hello.txt' });
await expect(fileLink).toBeVisible();
await expect(page.locator('code').first()).toHaveText('/'); // Current directory
});

test('uploads a file successfully with correct token', async ({ page }) => {
await page.goto('/');

// Create a temporary file to upload
const uploadFileName = 'upload-test.txt';
const uploadFilePath = path.join(TEST_DATA_DIR, '..', uploadFileName); // Outside the served root
fs.writeFileSync(uploadFilePath, 'some content');

// Fill the token
await page.locator('#token-input').fill('secret-token');

// Select file
const fileInput = page.locator('#file-input');
await fileInput.setInputFiles(uploadFilePath);

// Click upload
await page.getByRole('button', { name: 'Upload' }).click();

// Wait for the upload to complete and page to reload.
// The client calls window.location.reload() immediately after setting success text,
// so checking for "Upload complete" is race-prone.
// Instead, we wait for the uploaded file to appear in the list.

// Verify file is in the list
const uploadedLink = page.getByRole('link', { name: uploadFileName });
await expect(uploadedLink).toBeVisible();

// Cleanup
fs.unlinkSync(uploadFilePath);
const destinationPath = path.join(TEST_DATA_DIR, uploadFileName);
if (fs.existsSync(destinationPath)) fs.unlinkSync(destinationPath);
});

test('fails to upload with incorrect token', async ({ page }) => {
await page.goto('/');

const uploadFileName = 'fail-test.txt';
const uploadFilePath = path.join(TEST_DATA_DIR, '..', uploadFileName);
fs.writeFileSync(uploadFilePath, 'content');

// Fill WRONG token
await page.locator('#token-input').fill('wrong-token');

await page.locator('#file-input').setInputFiles(uploadFilePath);
await page.getByRole('button', { name: 'Upload' }).click();

// Wait for failure status
// The server returns 401 "missing/invalid upload token"
await expect(page.locator('#upload-status')).toContainText('Upload failed for fail-test.txt: missing/invalid upload token');

// Cleanup
fs.unlinkSync(uploadFilePath);
});

test('client-side prevents uploading files larger than limit', async ({ page }) => {
// server configured with max 100 bytes
await page.goto('/');

const bigFileName = 'big.txt';
const bigFilePath = path.join(TEST_DATA_DIR, '..', bigFileName);
// Create file > 100 bytes
const bigContent = 'a'.repeat(101);
fs.writeFileSync(bigFilePath, bigContent);

await page.locator('#token-input').fill('secret-token');
await page.locator('#file-input').setInputFiles(bigFilePath);
await page.getByRole('button', { name: 'Upload' }).click();

// Expect client-side blocking message
// JS logic: setStatus('Upload blocked for ' + file.name + ': file is ' + file.size + ' bytes (max ' + maxBytes + ').');
await expect(page.locator('#upload-status')).toContainText('Upload blocked for big.txt');
await expect(page.locator('#upload-status')).toContainText('max 100');

// Cleanup
fs.unlinkSync(bigFilePath);
});
});