shoumen 1 viikko sitten
vanhempi
commit
caaf3a5ba6

+ 222 - 15
config/users.js

@@ -1,38 +1,245 @@
-// Configuration for test users
+const SelectAction = require('../src/actions/select-action');
+const TypingAction = require('../src/actions/typing-action');
+const FormatAction = require('../src/actions/format-action');
+const WaitAction = require('../src/actions/wait-action');
+const PasteAction = require('../src/actions/paste-action');
+
 const users = [
 	{
 		login: 'Admin',
 		password: '11111111',
 		actions: [
-			{
+			// Начать с выделения всего текста
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 0,
+				end: 12
+			}),
+			new WaitAction({
+				delay: 400
+			}),
+			// Снять выделение, выделить только "Привет"
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 0,
+				end: 6
+			}),
+			new WaitAction({
+				delay: 600
+			}),
+			// Сделать "Привет" красным
+			new FormatAction({
+				formatType: 'foreground',
+				value: 0xFF0000
+			}),
+			new WaitAction({
+				delay: 300
+			}),
+			// Выделить запятую и пробел и заменить через paste
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 6,
+				end: 8
+			}),
+			new WaitAction({
+				delay: 250
+			}),
+			// Заменить выделенную область на " - "
+			new PasteAction({
+				text: ' - '
+			}),
+			new WaitAction({
+				delay: 500
+			}),
+			// Выделить "мир" и заменить на "вселенная"
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 10,
+				end: 13
+			}),
+			new WaitAction({
+				delay: 400
+			}),
+			// Заменить "мир" на "вселенная" через paste
+			new PasteAction({
+				text: 'вселенная'
+			}),
+			new WaitAction({
+				delay: 300
+			}),
+			// Выделить "вселенная" и сделать зеленой
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 10,
+				end: 19
+			}),
+			new FormatAction({
+				formatType: 'foreground',
+				value: 0x00FF00
+			}),
+			new WaitAction({
+				delay: 350
+			}),
+			// Переместиться в конец и добавить текст через paste
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 20,
+				end: 20
+			}),
+			new PasteAction({
+				text: '! Как дела?'
+			}),
+			new WaitAction({
+				delay: 700
+			}),
+			// Выделить добавленный текст и сделать синим
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 20,
+				end: 31
+			}),
+			new FormatAction({
+				formatType: 'foreground',
+				value: 0x0000FF
+			}),
+			new WaitAction({
+				delay: 400
+			}),
+			// Выделить "Привет" и заменить на "Салют"
+			new SelectAction({
 				firstParagraph: 0,
 				lastParagraph: 0,
-				start: 7,
-				end: 7,
-				text: 'АВТОМОБИЛЬ',
-				keyInterval: 1100
-			}
+				start: 0,
+				end: 6
+			}),
+			new PasteAction({
+				text: 'Салют'
+			})
 		]
 	},
 	{
 		login: 'Admin',
 		password: '11111111',
 		actions: [
-			{
+			// Вставить приветствие в начало
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 0,
+				end: 0
+			}),
+			new PasteAction({
+				text: 'Здравствуй, '
+			}),
+			new WaitAction({
+				delay: 800
+			}),
+			// Выделить оригинальное "Привет"
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 12,
+				end: 18
+			}),
+			new WaitAction({
+				delay: 500
+			}),
+			// Заменить на "дорогой" через paste
+			new PasteAction({
+				text: 'дорогой'
+			}),
+			new WaitAction({
+				delay: 600
+			}),
+			// Выделить "мир" и заменить на "друг" через paste
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 21,
+				end: 24
+			}),
+			new PasteAction({
+				text: 'друг'
+			}),
+			new WaitAction({
+				delay: 300
+			}),
+			// Выделить "друг" и сделать его фиолетовым
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 21,
+				end: 25
+			}),
+			new FormatAction({
+				formatType: 'foreground',
+				value: 0x800080
+			}),
+			new WaitAction({
+				delay: 400
+			}),
+			// Переместиться после "мир" и добавить восклицательный знак
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 24,
+				end: 24
+			}),
+			new TypingAction({
+				text: '!!!',
+				keyInterval: 300
+			}),
+			new WaitAction({
+				delay: 500
+			}),
+			// Выделить весь текст и применить оранжевый цвет фона
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 0,
+				end: 27
+			}),
+			new WaitAction({
+				delay: 300
+			}),
+			// Снять выделение и добавить смайлик в конец
+			new SelectAction({
+				firstParagraph: 0,
+				lastParagraph: 0,
+				start: 27,
+				end: 27
+			}),
+			new TypingAction({
+				text: ' @',
+				keyInterval: 100
+			}),
+			new WaitAction({
+				delay: 400
+			}),
+			// Выделить смайлик и сделать его желтым
+			new SelectAction({
 				firstParagraph: 0,
 				lastParagraph: 0,
-				start: 9,
-				end: 9,
-				text: 'трактор',
-				keyInterval: 1100
-			}
+				start: 28,
+				end: 29
+			}),
+			new FormatAction({
+				formatType: 'foreground',
+				value: 0xFFFF00
+			})
 		]
 	}
 ];
 
-// Test document configuration
 const testDocument = {
-	url: 'http://localhost:9080/debug.html#ZCZ1EW'
+	url: 'http://localhost:9080/debug.html#E4PVFP'
 };
 
 module.exports = {

+ 19 - 43
src/actions.js

@@ -1,4 +1,5 @@
 const { chromium } = require('@playwright/test');
+const DebuggerSync = require('./debugger-sync');
 
 class CollaborativeActions {
 	constructor(user, headless = false, userIndex = 0) {
@@ -8,20 +9,20 @@ class CollaborativeActions {
 		this.browser = null;
 		this.context = null;
 		this.page = null;
+		this.debuggerSync = 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 windowWidth = Math.floor(screenWidth / 2);
+		const windowHeight = screenHeight - 150;
 		const windowX = this.userIndex * windowWidth;
-		const windowY = 0; // Small offset from top
+		const windowY = 0;
 
 		this.browser = await chromium.launch({
 			headless: this.headless,
-			devtools: true, // Открывает DevTools и предотвращает закрытие окна при отладке
+			devtools: true,
 			args: [
 				'--no-sandbox',
 				'--disable-setuid-sandbox',
@@ -31,13 +32,21 @@ class CollaborativeActions {
 		});
 
 		this.context = await this.browser.newContext({
-			viewport: { width: windowWidth, height: windowHeight - 100 }, // Account for browser chrome
-			deviceScaleFactor: 1 // Ensure 1:1 pixel ratio
+			viewport: { width: windowWidth, height: windowHeight - 100 },
+			deviceScaleFactor: 1
 		});
 
 		this.page = await this.context.newPage();
 	}
 
+	async registerDebuggerSync(debuggerSync) {
+		this.debuggerSync = debuggerSync;
+		if (this.page && debuggerSync) {
+			const sessionId = `${this.user.login}_${this.userIndex}`;
+			await debuggerSync.registerSession(this.page, sessionId);
+		}
+	}
+
 	async login(loginUrl = 'http://localhost:9080/debug.html#XKJ7L2') {
 		try {
 			await this.page.goto(loginUrl);
@@ -95,48 +104,15 @@ class CollaborativeActions {
 			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);
+				// Execute the action directly (action is already an instance)
+				await action.execute(this.page);
 
 			} catch(error) {
-				// Silent error handling
+				console.error(`Error executing action ${i}:`, error);
 			}
 		}
 	}
 
-	// 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();

+ 11 - 0
src/actions/base-action.js

@@ -0,0 +1,11 @@
+class Action {
+	constructor(actionData) {
+		this.actionData = actionData;
+	}
+
+	async execute(page) {
+		throw new Error('execute() method must be implemented by subclasses');
+	}
+}
+
+module.exports = Action;

+ 31 - 0
src/actions/format-action.js

@@ -0,0 +1,31 @@
+const Action = require('./base-action');
+
+class FormatAction extends Action {
+	constructor(actionData) {
+		super(actionData);
+		this.formatType = actionData.formatType;
+		this.value = actionData.value;
+	}
+
+	async execute(page) {
+		console.log('FormatAction executing with:', this.formatType, this.value);
+		try {
+			await page.evaluate((actionData) => {
+				try {
+					const formatType = actionData.formatType;
+					const value = actionData.value;
+
+					console.log('About to dispatch:', formatType, value);
+					XCommand.dispatch(formatType, new Color(value));
+					console.log('XCommand.dispatch completed');
+				} catch (error) {
+					console.error('Error applying formatting:', error);
+				}
+			}, this.actionData);
+		} catch (error) {
+			console.error('Error executing FormatAction:', error);
+		}
+	}
+}
+
+module.exports = FormatAction;

+ 27 - 0
src/actions/paste-action.js

@@ -0,0 +1,27 @@
+const Action = require('./base-action');
+
+class PasteAction extends Action {
+	constructor(actionData) {
+		super(actionData);
+		this.text = actionData.text;
+	}
+
+	async execute(page) {
+		try {
+			if (this.text) {
+				await page.evaluate((text) => {
+					try {
+						const target = window.MainFrame.getItems().last().getEditor().getTarget();
+						target.paste(PasteContent.create(text));
+					} catch (error) {
+						console.error('Error pasting text:', error);
+					}
+				}, this.text);
+			}
+		} catch (error) {
+			console.error('Error executing PasteAction:', error);
+		}
+	}
+}
+
+module.exports = PasteAction;

+ 36 - 0
src/actions/select-action.js

@@ -0,0 +1,36 @@
+const Action = require('./base-action');
+
+class SelectAction extends Action {
+	constructor(actionData) {
+		super(actionData);
+		this.firstParagraph = actionData.firstParagraph;
+		this.lastParagraph = actionData.lastParagraph;
+		this.start = actionData.start;
+		this.end = actionData.end;
+	}
+
+	async execute(page) {
+		try {
+			await page.evaluate((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);
+				}
+			}, this.actionData);
+			await page.waitForTimeout(200);
+		} catch (error) {
+			console.error('Error executing SelectAction:', error);
+		}
+	}
+}
+
+module.exports = SelectAction;

+ 31 - 0
src/actions/typing-action.js

@@ -0,0 +1,31 @@
+const Action = require('./base-action');
+
+class TypingAction extends Action {
+	constructor(actionData) {
+		super(actionData);
+		this.text = actionData.text;
+		this.keyInterval = actionData.keyInterval || 300;
+	}
+
+	async execute(page) {
+		try {
+			if (this.text && this.text.length > 0) {
+				await this.typeTextCharByChar(page, this.text, this.keyInterval);
+			}
+			
+			await page.waitForTimeout(300);
+		} catch (error) {
+		}
+	}
+
+	async typeTextCharByChar(page, text, delay) {
+		for (let i = 0; i < text.length; i++) {
+			await page.keyboard.type(text[i]);
+			if (delay > 0) {
+				await page.waitForTimeout(delay);
+			}
+		}
+	}
+}
+
+module.exports = TypingAction;

+ 20 - 0
src/actions/wait-action.js

@@ -0,0 +1,20 @@
+const Action = require('./base-action');
+
+class WaitAction extends Action {
+	constructor(actionData) {
+		super(actionData);
+		this.delay = actionData.delay || 1000; // Default to 1 second if not specified
+	}
+
+	async execute(page) {
+		console.log(`WaitAction executing with delay: ${this.delay}ms`);
+		try {
+			await new Promise(resolve => setTimeout(resolve, this.delay));
+			console.log(`WaitAction completed after ${this.delay}ms delay`);
+		} catch (error) {
+			console.error('Error executing WaitAction:', error);
+		}
+	}
+}
+
+module.exports = WaitAction;

+ 221 - 0
src/debugger-sync.js

@@ -0,0 +1,221 @@
+const { chromium } = require('@playwright/test');
+
+class DebuggerSync {
+	constructor() {
+		this.sessions = [];
+		this.breakpoints = new Map();
+		this.isEnabled = false;
+	}
+
+	async registerSession(page, sessionId) {
+		try {
+			const cdpSession = await page.context().newCDPSession(page);
+
+			await cdpSession.send('Debugger.enable');
+			await cdpSession.send('Runtime.enable');
+
+			const session = {
+				id: sessionId,
+				page: page,
+				cdp: cdpSession,
+				breakpoints: new Set()
+			};
+
+			cdpSession.on('Debugger.paused', (params) => {
+				this.onDebuggerPaused(sessionId, params);
+			});
+
+			cdpSession.on('Debugger.resumed', (params) => {
+				this.onDebuggerResumed(sessionId, params);
+			});
+
+			this.sessions.push(session);
+
+			return session;
+		} catch (error) {
+			return null;
+		}
+	}
+
+	async enableSync() {
+		this.isEnabled = true;
+
+		// Настраиваем перехват точек останова для всех сессий
+		for (const session of this.sessions) {
+			await this.setupBreakpointInterception(session);
+		}
+	}
+
+	// Настраиваем перехват точек останова
+	async setupBreakpointInterception(session) {
+		try {
+			// Перехватываем установку точек останова через DevTools UI
+			await session.page.addInitScript(() => {
+				// Перехватываем методы DevTools для установки breakpoints
+				if (window.DevToolsAPI) {
+					const originalSetBreakpoint = window.DevToolsAPI.setBreakpoint;
+					window.DevToolsAPI.setBreakpoint = function(...args) {
+						// Отправляем событие о новой точке останова
+						window.postMessage({
+							type: 'BREAKPOINT_SET',
+							data: args
+						}, '*');
+						return originalSetBreakpoint.apply(this, args);
+					};
+				}
+			});
+
+			// Слушаем сообщения о точках останова
+			await session.page.exposeFunction('notifyBreakpointChange', (data) => {
+				this.handleBreakpointChange(session.id, data);
+			});
+
+		} catch (error) {
+			console.error(`Failed to setup breakpoint interception for ${session.id}:`, error);
+		}
+	}
+
+	// Обработка изменения точек останова
+	async handleBreakpointChange(sessionId, data) {
+		if (!this.isEnabled) return;
+
+		const { url, lineNumber, columnNumber, condition } = data;
+		const breakpointId = `${url}:${lineNumber}:${columnNumber}`;
+
+		// Сохраняем точку останова
+		this.breakpoints.set(breakpointId, {
+			url,
+			lineNumber,
+			columnNumber,
+			condition,
+			setBy: sessionId
+		});
+
+		// Синхронизируем с другими сессиями
+		await this.syncBreakpointToOtherSessions(sessionId, breakpointId, data);
+	}
+
+	// Синхронизируем точку останова с другими сессиями
+	async syncBreakpointToOtherSessions(originSessionId, breakpointId, breakpointData) {
+		const { url, lineNumber, columnNumber, condition } = breakpointData;
+
+		for (const session of this.sessions) {
+			if (session.id === originSessionId) continue;
+
+			try {
+				// Устанавливаем точку останова через CDP
+				const response = await session.cdp.send('Debugger.setBreakpointByUrl', {
+					lineNumber: lineNumber - 1, // CDP использует 0-based индексацию
+					url: url,
+					columnNumber: columnNumber,
+					condition: condition
+				});
+
+				session.breakpoints.add(breakpointId);
+				console.log(`🔄 Synced breakpoint ${breakpointId} to session ${session.id}`);
+
+			} catch (error) {
+				console.error(`Failed to sync breakpoint to session ${session.id}:`, error);
+			}
+		}
+	}
+
+	// Обработка паузы отладчика
+	async onDebuggerPaused(sessionId, params) {
+		if (!this.isEnabled) return;
+
+		console.log(`⏸️  Debugger paused in session ${sessionId}`);
+
+		// Приостанавливаем выполнение во всех других сессиях
+		await this.pauseAllOtherSessions(sessionId);
+	}
+
+	// Обработка возобновления отладчика
+	async onDebuggerResumed(sessionId, params) {
+		if (!this.isEnabled) return;
+
+		console.log(`▶️  Debugger resumed in session ${sessionId}`);
+
+		// Возобновляем выполнение во всех других сессиях
+		await this.resumeAllOtherSessions(sessionId);
+	}
+
+	// Приостанавливаем все остальные сессии
+	async pauseAllOtherSessions(originSessionId) {
+		const pausePromises = this.sessions
+			.filter(session => session.id !== originSessionId)
+			.map(async session => {
+				try {
+					await session.cdp.send('Debugger.pause');
+					console.log(`⏸️  Paused session ${session.id}`);
+				} catch (error) {
+					console.error(`Failed to pause session ${session.id}:`, error);
+				}
+			});
+
+		await Promise.all(pausePromises);
+	}
+
+	// Возобновляем все остальные сессии
+	async resumeAllOtherSessions(originSessionId) {
+		const resumePromises = this.sessions
+			.filter(session => session.id !== originSessionId)
+			.map(async session => {
+				try {
+					await session.cdp.send('Debugger.resume');
+					console.log(`▶️  Resumed session ${session.id}`);
+				} catch (error) {
+					console.error(`Failed to resume session ${session.id}:`, error);
+				}
+			});
+
+		await Promise.all(resumePromises);
+	}
+
+	// Удаляем точку останова из всех сессий
+	async removeBreakpoint(breakpointId) {
+		this.breakpoints.delete(breakpointId);
+
+		for (const session of this.sessions) {
+			try {
+				// Находим и удаляем точку останова
+				const [url, lineNumber] = breakpointId.split(':');
+				await session.cdp.send('Debugger.removeBreakpoint', {
+					breakpointId: breakpointId
+				});
+
+				session.breakpoints.delete(breakpointId);
+				console.log(`🗑️  Removed breakpoint ${breakpointId} from session ${session.id}`);
+
+			} catch (error) {
+				console.error(`Failed to remove breakpoint from session ${session.id}:`, error);
+			}
+		}
+	}
+
+	// Отключаем синхронизацию
+	async disableSync() {
+		this.isEnabled = false;
+		console.log('🔄 Breakpoint synchronization disabled');
+	}
+
+	// Очистка ресурсов
+	async cleanup() {
+		this.isEnabled = false;
+
+		for (const session of this.sessions) {
+			try {
+				await session.cdp.send('Debugger.disable');
+				await session.cdp.detach();
+			} catch (error) {
+				console.error(`Error during cleanup for session ${session.id}:`, error);
+			}
+		}
+
+		this.sessions = [];
+		this.breakpoints.clear();
+		console.log('🧹 Debugger sync cleaned up');
+	}
+}
+
+module.exports = DebuggerSync;

+ 24 - 12
src/index.js

@@ -2,14 +2,15 @@ const fs = require('fs');
 const path = require('path');
 const readline = require('readline');
 const CollaborativeActions = require('./actions');
+const DebuggerSync = require('./debugger-sync');
 const { users, testDocument } = require('../config/users');
 
 class CollaborationTestOrchestrator {
 	constructor() {
 		this.userSessions = [];
 		this.testResults = [];
+		this.debuggerSync = new DebuggerSync();
 
-		// Create screenshots directory if it doesn't exist
 		if(!fs.existsSync('screenshots')) {
 			fs.mkdirSync('screenshots');
 		}
@@ -17,32 +18,27 @@ class CollaborationTestOrchestrator {
 
 	async runCollaborationTest() {
 		try {
-			// Initialize all user sessions
 			await this.initializeUsers();
 
-			// Run the authentication test
+			await this.setupDebuggerSync();
+
 			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
@@ -56,12 +52,24 @@ class CollaborationTestOrchestrator {
 		await Promise.all(documentPromises);
 	}
 
+	async setupDebuggerSync() {
+		console.log('');
+		
+		const registrationPromises = this.userSessions.map(session => 
+			session.registerDebuggerSync(this.debuggerSync)
+		);
+		await Promise.all(registrationPromises);
+
+		await this.debuggerSync.enableSync();
+		
+		console.log('');
+		console.log('');
+	}
+
 	async executeTestScenario() {
-		// Wait for user input before executing actions
-		console.log('⏳ Press Enter to start executing user actions...');
+		console.log('');
 		await this.waitForUserInput();
 
-		// Execute user actions after authentication
 		const actionPromises = this.userSessions.map(session => session.executeUserActions());
 		await Promise.all(actionPromises);
 	}
@@ -93,6 +101,11 @@ class CollaborationTestOrchestrator {
 	}
 
 	async cleanup() {
+		// Очищаем синхронизатор отладчика
+		if (this.debuggerSync) {
+			await this.debuggerSync.cleanup();
+		}
+		
 		const cleanupPromises = this.userSessions.map(session => session.cleanup());
 		await Promise.all(cleanupPromises);
 	}
@@ -119,7 +132,6 @@ class CollaborationTestOrchestrator {
 	}
 }
 
-// Run the test if this file is executed directly
 if(require.main === module) {
 	const orchestrator = new CollaborationTestOrchestrator();
 	orchestrator.runCollaborationTest()