shoumen 1 周之前
当前提交
7328b06cb0
共有 7 个文件被更改,包括 553 次插入0 次删除
  1. 28 0
      .gitignore
  2. 166 0
      README.md
  3. 41 0
      config/users.js
  4. 22 0
      examples/custom-scenario.js
  5. 16 0
      package.json
  6. 146 0
      src/actions.js
  7. 134 0
      src/index.js

+ 28 - 0
.gitignore

@@ -0,0 +1,28 @@
+# Dependencies
+node_modules/
+package-lock.json
+
+# IDE files
+.idea/
+*.iml
+
+# Test results and screenshots
+screenshots/
+test-report.json
+
+# Logs
+*.log
+npm-debug.log*
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Temporary files
+*.tmp
+*.temp

+ 166 - 0
README.md

@@ -0,0 +1,166 @@
+# Collaborative Document Testing System
+
+Система тестирования совместной работы с документами на Node.js с использованием Playwright.
+
+## 🚀 Возможности
+
+- **Множественные браузеры**: Автоматически открывает несколько браузеров
+- **Авторизация пользователей**: Логинится под разными учётными записями
+- **Совместное редактирование**: Симулирует одновременную работу пользователей
+- **Автоматические сценарии**: Выполняет заранее заданные действия
+- **Скриншоты**: Делает снимки экрана на каждом этапе тестирования
+- **Отчёты**: Генерирует подробные отчёты о тестировании
+
+## 📁 Структура проекта
+
+```
+collaboration-test/
+├── package.json                 # Зависимости проекта
+├── README.md                   # Документация
+├── config/
+│   └── users.js               # Конфигурация пользователей
+├── src/
+│   ├── index.js              # Основной оркестратор тестов
+│   └── actions.js            # Модуль действий браузера
+├── examples/
+│   └── custom-scenario.js    # Пример кастомного сценария
+└── screenshots/              # Папка для скриншотов (создаётся автоматически)
+```
+
+## 🛠 Установка
+
+1. **Клонируйте проект и установите зависимости:**
+```bash
+npm install
+```
+
+2. **Установите браузеры Playwright:**
+```bash
+npm run install-browsers
+```
+
+## ⚙️ Настройка
+
+1. **Настройте пользователей** в `config/users.js`:
+```javascript
+const users = [
+  {
+    id: 'user1',
+    name: 'Alice',
+    email: 'your-test-email1@gmail.com',
+    password: 'your-password',
+    role: 'editor'
+  },
+  // ... добавьте больше пользователей
+];
+```
+
+2. **Настройте тестовый документ** в том же файле:
+```javascript
+const testDocument = {
+  url: 'https://docs.google.com/document/d/YOUR-DOCUMENT-ID/edit',
+  title: 'Ваш тестовый документ'
+};
+```
+
+## 🎯 Использование
+
+### Базовый тест
+```bash
+npm test
+```
+
+### Кастомный сценарий
+```bash
+node examples/custom-scenario.js
+```
+
+### Программное использование
+```javascript
+const CollaborationTestOrchestrator = require('./src/index');
+
+const orchestrator = new CollaborationTestOrchestrator();
+orchestrator.runCollaborationTest();
+```
+
+## 📋 Сценарии тестирования
+
+### Встроенные сценарии:
+1. **Одновременный ввод текста** - все пользователи печатают одновременно
+2. **Последовательное комментирование** - пользователи по очереди добавляют комментарии
+3. **Смешанные действия** - комбинация редактирования и комментирования
+
+### Кастомные сценарии:
+- **Race condition тест** - проверка конфликтов при одновременном редактировании
+- **Цепочка комментариев** - симуляция обсуждения
+- **Паттерны поведения** - различное поведение в зависимости от роли пользователя
+
+## 📊 Результаты
+
+После выполнения тестов вы получите:
+- **Скриншоты** в папке `screenshots/`
+- **JSON отчёт** в файле `test-report.json`
+- **Консольный вывод** с подробной информацией о процессе
+
+## 🔧 Настройка действий
+
+Класс `CollaborativeActions` поддерживает следующие методы:
+
+```javascript
+// Базовые действия
+await session.initialize();           // Инициализация браузера
+await session.login();               // Авторизация
+await session.openDocument(url);     // Открытие документа
+
+// Действия с контентом
+await session.typeText(text);        // Ввод текста
+await session.simulateTyping(5000);  // Симуляция печати
+await session.addComment(text);      // Добавление комментария
+
+// Утилиты
+await session.takeScreenshot(name);  // Скриншот
+await session.cleanup();            // Очистка ресурсов
+```
+
+## 🚨 Важные замечания
+
+1. **Безопасность**: Не используйте реальные пароли в production
+2. **Лимиты API**: Учитывайте ограничения сервисов (Google Docs, etc.)
+3. **Браузеры**: Тесты запускаются в видимом режиме для наблюдения
+4. **Таймауты**: При медленном интернете увеличьте таймауты в коде
+
+## 🛡️ Требования
+
+- Node.js 16+
+- Доступ к интернету
+- Тестовые аккаунты Google (для Google Docs)
+- Тестовый документ с правами на редактирование
+
+## 📝 Примеры использования
+
+### Создание кастомного сценария:
+```javascript
+class MyCustomTest extends CollaborationTestOrchestrator {
+  async executeTestScenario() {
+    // Ваш кастомный сценарий
+    await this.myCustomAction();
+  }
+  
+  async myCustomAction() {
+    for (const session of this.userSessions) {
+      await session.typeText('Мой текст');
+    }
+  }
+}
+```
+
+## 🤝 Вклад в проект
+
+1. Fork проекта
+2. Создайте feature branch
+3. Внесите изменения
+4. Создайте Pull Request
+
+## 📄 Лицензия
+
+MIT License

+ 41 - 0
config/users.js

@@ -0,0 +1,41 @@
+// Configuration for test users
+const users = [
+	{
+		login: 'Admin',
+		password: '11111111',
+		actions: [
+			{
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 7,
+				end: 7,
+				text: 'АВТОМОБИЛЬ',
+				keyInterval: 1100
+			}
+		]
+	},
+	{
+		login: 'Admin',
+		password: '11111111',
+		actions: [
+			{
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 9,
+				end: 9,
+				text: 'трактор',
+				keyInterval: 1100
+			}
+		]
+	}
+];
+
+// Test document configuration
+const testDocument = {
+	url: 'http://localhost:9080/debug.html#ZCZ1EW'
+};
+
+module.exports = {
+	users,
+	testDocument
+};

+ 22 - 0
examples/custom-scenario.js

@@ -0,0 +1,22 @@
+const CollaborationTestOrchestrator = require('../src/index');
+const CollaborativeActions = require('../src/actions');
+
+class CustomTestScenario extends CollaborationTestOrchestrator {
+	async executeTestScenario() {
+		// Only authentication - no other test cases
+	}
+}
+
+// Run custom scenario if this file is executed directly
+if(require.main === module) {
+	const customTest = new CustomTestScenario();
+	customTest.runCollaborationTest()
+		.then(() => {
+			process.exit(0);
+		})
+		.catch((error) => {
+			process.exit(1);
+		});
+}
+
+module.exports = CustomTestScenario;

+ 16 - 0
package.json

@@ -0,0 +1,16 @@
+{
+  "name": "collaboration-test",
+  "version": "1.0.0",
+  "description": "Collaborative document testing system using Playwright",
+  "main": "src/index.js",
+  "scripts": {
+    "test": "node src/index.js",
+    "install-browsers": "npx playwright install"
+  },
+  "keywords": ["playwright", "testing", "collaboration", "document"],
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "@playwright/test": "^1.40.0"
+  }
+}

+ 146 - 0
src/actions.js

@@ -0,0 +1,146 @@
+const { chromium } = require('@playwright/test');
+
+class CollaborativeActions {
+	constructor(user, headless = false, userIndex = 0) {
+		this.user = user;
+		this.headless = headless;
+		this.userIndex = userIndex;
+		this.browser = null;
+		this.context = null;
+		this.page = null;
+	}
+
+	async initialize() {
+		// Calculate window position and size for side-by-side layout
+		const screenWidth = 1520;
+		const screenHeight = 1080;
+		const windowWidth = Math.floor(screenWidth / 2); // 1280px per window
+		const windowHeight = screenHeight - 150; // Leave space for dock/menu bar (1450px)
+		const windowX = this.userIndex * windowWidth;
+		const windowY = 0; // Small offset from top
+
+		this.browser = await chromium.launch({
+			headless: this.headless,
+			devtools: true, // Открывает DevTools и предотвращает закрытие окна при отладке
+			args: [
+				'--no-sandbox',
+				'--disable-setuid-sandbox',
+				`--window-size=${ windowWidth },${ windowHeight }`,
+				`--window-position=${ windowX },${ windowY }`
+			]
+		});
+
+		this.context = await this.browser.newContext({
+			viewport: { width: windowWidth, height: windowHeight - 100 }, // Account for browser chrome
+			deviceScaleFactor: 1 // Ensure 1:1 pixel ratio
+		});
+
+		this.page = await this.context.newPage();
+	}
+
+	async login(loginUrl = 'http://localhost:9080/debug.html#XKJ7L2') {
+		try {
+			await this.page.goto(loginUrl);
+
+			// Wait for login input and fill it using the specified selector
+			await this.page.waitForSelector('.signin.control-group .row:first-child input', { timeout: 10000 });
+			await this.page.fill('.signin.control-group .row:first-child input', this.user.login);
+
+			// Wait for password input and fill it using the specified selector
+			await this.page.waitForSelector('.signin.control-group .row:last-child input', { timeout: 10000 });
+			await this.page.fill('.signin.control-group .row:last-child input', this.user.password);
+
+			// Submit the form (assuming there's a submit button or Enter key works)
+			await this.page.keyboard.press('Enter');
+
+			// Wait for login to complete
+			await this.page.waitForTimeout(3000);
+		} catch(error) {
+			throw error;
+		}
+	}
+
+	async openDocument(documentUrl) {
+		await this.page.goto(documentUrl);
+	}
+
+	async typeText(text, delay = 100) {
+		await this.page.keyboard.type(text, { delay });
+		await this.page.waitForTimeout(1000);
+	}
+
+	async simulateTyping(duration = 5000) {
+		const words = ['Hello', 'world', 'this', 'is', 'a', 'test', 'of', 'collaborative', 'editing'];
+		const endTime = Date.now() + duration;
+
+		while(Date.now() < endTime) {
+			const randomWord = words[Math.floor(Math.random() * words.length)];
+			await this.typeText(randomWord + ' ', 200);
+			await this.page.waitForTimeout(Math.random() * 2000 + 500);
+		}
+	}
+
+	async takeScreenshot(filename) {
+		const screenshotPath = `screenshots/${ this.user.login }_${ filename }_${ Date.now() }.png`;
+		await this.page.screenshot({ path: screenshotPath });
+		return screenshotPath;
+	}
+
+	async executeUserActions() {
+		if(!this.user.actions || this.user.actions.length === 0) {
+			return;
+		}
+
+		for(let i = 0; i < this.user.actions.length; i++) {
+			const action = this.user.actions[i];
+
+			try {
+				// Execute the function with action parameters passed through context
+				await this.page.evaluate(this.selectEditorRange, action);
+
+				// Small delay after selection
+				await this.page.waitForTimeout(200);
+
+				// If action contains text, type it character by character
+				if(action.text && action.text.length > 0) {
+					const keyInterval = action.keyInterval || 300; // Use custom interval or default 300ms
+					await this.typeTextCharByChar(action.text, keyInterval);
+				}
+
+				// Small delay between actions
+				await this.page.waitForTimeout(300);
+
+			} catch(error) {
+				// Silent error handling
+			}
+		}
+	}
+
+	// Type text character by character with minimal delay
+	async typeTextCharByChar(text, delay) {
+		for(let i = 0; i < text.length; i++) {
+			await this.page.keyboard.type(text[i]);
+			if(delay > 0)
+				await this.page.waitForTimeout(delay);
+		}
+	}
+
+	// Proper JavaScript function to be executed in browser context
+	selectEditorRange(actionData) {
+		try {
+			const editor = window.MainFrame.getItems().last().getEditor();
+			const targetState = editor.getTargetState().copy().setFirstParagraphIndex(actionData.firstParagraph).setLastParagraphIndex(actionData.lastParagraph).setStart(actionData.start).setEnd(actionData.end);
+
+			editor.select(targetState);
+		} catch(error) {
+			console.error('Error selecting editor range:', error);
+		}
+	}
+
+	async cleanup() {
+		if(this.browser)
+			await this.browser.close();
+	}
+}
+
+module.exports = CollaborativeActions;

+ 134 - 0
src/index.js

@@ -0,0 +1,134 @@
+const fs = require('fs');
+const path = require('path');
+const readline = require('readline');
+const CollaborativeActions = require('./actions');
+const { users, testDocument } = require('../config/users');
+
+class CollaborationTestOrchestrator {
+	constructor() {
+		this.userSessions = [];
+		this.testResults = [];
+
+		// Create screenshots directory if it doesn't exist
+		if(!fs.existsSync('screenshots')) {
+			fs.mkdirSync('screenshots');
+		}
+	}
+
+	async runCollaborationTest() {
+		try {
+			// Initialize all user sessions
+			await this.initializeUsers();
+
+			// Run the authentication test
+			await this.executeTestScenario();
+
+			// Generate test report
+			this.generateReport();
+
+			await this.waitIndefinitely();
+
+		} catch(error) {
+			// Silent error handling
+		}
+		// Removed cleanup - browsers stay open
+	}
+
+	async initializeUsers() {
+		// Create all sessions first
+		const sessionPromises = users.map(async (user, index) => {
+			const session = new CollaborativeActions(user, false, index); // Pass userIndex for positioning
+			await session.initialize();
+			return session;
+		});
+
+		// Wait for all browsers to initialize
+		this.userSessions = await Promise.all(sessionPromises);
+
+		// Login all users simultaneously
+		const loginPromises = this.userSessions.map(session => session.login());
+		await Promise.all(loginPromises);
+
+		// Open documents for all users simultaneously
+		const documentPromises = this.userSessions.map(session =>
+			session.openDocument(testDocument.url)
+		);
+		await Promise.all(documentPromises);
+	}
+
+	async executeTestScenario() {
+		// Wait for user input before executing actions
+		console.log('⏳ Press Enter to start executing user actions...');
+		await this.waitForUserInput();
+
+		// Execute user actions after authentication
+		const actionPromises = this.userSessions.map(session => session.executeUserActions());
+		await Promise.all(actionPromises);
+	}
+
+	async takeAllScreenshots(suffix) {
+		const screenshotPromises = this.userSessions.map(session =>
+			session.takeScreenshot(suffix)
+		);
+		await Promise.all(screenshotPromises);
+	}
+
+	generateReport() {
+		const report = {
+			timestamp: new Date().toISOString(),
+			testDocument: testDocument,
+			users: users.map(user => ({
+				id: user.id,
+				name: user.name,
+				role: user.role
+			})),
+			scenarios: [
+				'Authentication test'
+			],
+			status: 'completed',
+			duration: 'approximately 30 seconds'
+		};
+
+		fs.writeFileSync('test-report.json', JSON.stringify(report, null, 2));
+	}
+
+	async cleanup() {
+		const cleanupPromises = this.userSessions.map(session => session.cleanup());
+		await Promise.all(cleanupPromises);
+	}
+
+	async waitIndefinitely() {
+		// Keep the process alive without closing browsers
+		return new Promise(() => {
+			// This promise never resolves, keeping browsers open
+		});
+	}
+
+	async waitForUserInput() {
+		const rl = readline.createInterface({
+			input: process.stdin,
+			output: process.stdout
+		});
+
+		return new Promise((resolve) => {
+			rl.question('', () => {
+				rl.close();
+				resolve();
+			});
+		});
+	}
+}
+
+// Run the test if this file is executed directly
+if(require.main === module) {
+	const orchestrator = new CollaborationTestOrchestrator();
+	orchestrator.runCollaborationTest()
+		.then(() => {
+			process.exit(0);
+		})
+		.catch((error) => {
+			process.exit(1);
+		});
+}
+
+module.exports = CollaborationTestOrchestrator;