From ae1f9adf4f3d7aaa5990b0ed3eb084f187c736d9 Mon Sep 17 00:00:00 2001 From: toddtarsi Date: Thu, 4 Apr 2024 08:24:42 -0500 Subject: [PATCH] new functions, undo redo reload all have hotkeys, reload needs an icon button --- packages/selenium-ide/package.json | 2 +- .../PlaybackWindow/preload/recorder.ts | 5 +- .../PlaybackWindowBidi/preload/recorder.ts | 15 ++++-- .../ProjectEditor/tabs/Project/ProjectTab.tsx | 6 +-- .../main/api/classes/DriverEventListener.ts | 18 ++++--- .../src/main/api/classes/DriverHandler.ts | 17 ++---- .../src/main/api/classes/DriverRawHandler.ts | 29 +++------- .../src/main/api/classes/EventListener.ts | 11 ++-- .../src/main/api/classes/Handler.ts | 21 +++----- .../src/main/api/classes/RawHandler.ts | 29 +++------- packages/selenium-ide/src/main/index.ts | 5 +- .../controllers/Menu/menus/application.ts | 6 +-- .../controllers/Menu/menus/editBasics.ts | 18 +++++-- .../controllers/Menu/menus/projectEditor.ts | 2 +- .../controllers/Menu/menus/projectView.ts | 21 ++++++++ .../controllers/Menu/menus/textField.ts | 10 ---- .../session/controllers/Playback/index.ts | 12 ++++- .../session/controllers/Polyfill/index.ts | 10 +--- .../session/controllers/Recorder/index.ts | 9 ++-- .../main/session/controllers/State/index.ts | 54 ++++++++++++++++++- .../main/session/controllers/System/index.ts | 15 ++++-- .../main/session/controllers/Windows/index.ts | 15 +++++- packages/selenium-ide/webpack.config.ts | 42 +++++---------- 23 files changed, 206 insertions(+), 166 deletions(-) diff --git a/packages/selenium-ide/package.json b/packages/selenium-ide/package.json index a6f0dea46..cd81d84c7 100644 --- a/packages/selenium-ide/package.json +++ b/packages/selenium-ide/package.json @@ -1,6 +1,6 @@ { "name": "selenium-ide", - "version": "4.0.1-beta.4", + "version": "4.0.1-beta.5", "private": false, "description": "Selenium IDE electron app", "author": "Todd ", diff --git a/packages/selenium-ide/src/browser/windows/PlaybackWindow/preload/recorder.ts b/packages/selenium-ide/src/browser/windows/PlaybackWindow/preload/recorder.ts index 3e9733355..21b249059 100644 --- a/packages/selenium-ide/src/browser/windows/PlaybackWindow/preload/recorder.ts +++ b/packages/selenium-ide/src/browser/windows/PlaybackWindow/preload/recorder.ts @@ -24,7 +24,10 @@ import { ExpandedMutationObserver, } from 'browser/types' import initFindSelect from './find-select' -import { RecordNewCommandInput, RecorderPreprocessor } from '@seleniumhq/side-api' +import { + RecordNewCommandInput, + RecorderPreprocessor, +} from '@seleniumhq/side-api' import LocatorBuilders from './locator-builders' export interface RecordingState { diff --git a/packages/selenium-ide/src/browser/windows/PlaybackWindowBidi/preload/recorder.ts b/packages/selenium-ide/src/browser/windows/PlaybackWindowBidi/preload/recorder.ts index e6b7d3ab0..80a31b08c 100644 --- a/packages/selenium-ide/src/browser/windows/PlaybackWindowBidi/preload/recorder.ts +++ b/packages/selenium-ide/src/browser/windows/PlaybackWindowBidi/preload/recorder.ts @@ -15,11 +15,20 @@ // specific language governing permissions and limitations // under the License. -import { RecordNewCommandInput, RecorderPreprocessor } from '@seleniumhq/side-api' +import { + RecordNewCommandInput, + RecorderPreprocessor, +} from '@seleniumhq/side-api' import initFindSelect from 'browser/windows/PlaybackWindow/preload/find-select' import LocatorBuilders from 'browser/windows/PlaybackWindow/preload/locator-builders' -import { attach, detach } from 'browser/windows/PlaybackWindow/preload/prompt-injector' -import { handlers, observers } from 'browser/windows/PlaybackWindow/preload/record-handlers' +import { + attach, + detach, +} from 'browser/windows/PlaybackWindow/preload/prompt-injector' +import { + handlers, + observers, +} from 'browser/windows/PlaybackWindow/preload/record-handlers' import { EventHandler, ExpandedMessageEvent, diff --git a/packages/selenium-ide/src/browser/windows/ProjectEditor/tabs/Project/ProjectTab.tsx b/packages/selenium-ide/src/browser/windows/ProjectEditor/tabs/Project/ProjectTab.tsx index 220df48c2..c37c35959 100644 --- a/packages/selenium-ide/src/browser/windows/ProjectEditor/tabs/Project/ProjectTab.tsx +++ b/packages/selenium-ide/src/browser/windows/ProjectEditor/tabs/Project/ProjectTab.tsx @@ -14,11 +14,7 @@ export interface MiniProjectShape { } const SettingsWrapper: FC = () => - useContext(context) === 'project' ? ( - - ) : ( - - ) + useContext(context) === 'project' ? : const ProjectTab: React.FC> = () => ( diff --git a/packages/selenium-ide/src/main/api/classes/DriverEventListener.ts b/packages/selenium-ide/src/main/api/classes/DriverEventListener.ts index a6aa6887f..68c228115 100644 --- a/packages/selenium-ide/src/main/api/classes/DriverEventListener.ts +++ b/packages/selenium-ide/src/main/api/classes/DriverEventListener.ts @@ -1,10 +1,12 @@ import { ipcMain, WebContents } from 'electron' -import { BaseListener, EventMutator, ListenerFn, VariadicArgs } from '@seleniumhq/side-api' +import { + BaseListener, + EventMutator, + ListenerFn, + VariadicArgs, +} from '@seleniumhq/side-api' import { Session } from 'main/types' import getCore from '../helpers/getCore' -import { COLOR_CYAN, vdebuglog } from 'main/util' - -const apiDebugLog = vdebuglog('api', COLOR_CYAN) const baseListener = ( path: string, @@ -14,11 +16,11 @@ const baseListener = ( const listeners: any[] = [] return { addListener(listener) { - apiDebugLog('Listener added', path) + session.system.loggers.api('Listener added', path) listeners.push(listener) }, async dispatchEvent(...args) { - apiDebugLog('Dispatch event', path, args) + session.system.loggers.api('Dispatch event', path, args) if (mutator) { const newState = mutator(getCore(session), args) session.projects.project = newState.project @@ -26,7 +28,7 @@ const baseListener = ( session.api.state.onMutate.dispatchEvent(path, args) } const results = await Promise.all(listeners.map((fn) => fn(...args))) - return results; + return results }, hasListener(listener) { return listeners.includes(listener) @@ -37,7 +39,7 @@ const baseListener = ( if (index === -1) { throw new Error(`Unable to remove listener for ${path} ${listener}`) } - apiDebugLog('Listener removed', path) + session.system.loggers.api('Listener removed', path) listeners.splice(index, 1) }, } diff --git a/packages/selenium-ide/src/main/api/classes/DriverHandler.ts b/packages/selenium-ide/src/main/api/classes/DriverHandler.ts index 1d2cf60f3..fdf0bc42a 100644 --- a/packages/selenium-ide/src/main/api/classes/DriverHandler.ts +++ b/packages/selenium-ide/src/main/api/classes/DriverHandler.ts @@ -6,12 +6,8 @@ import { Mutator, } from '@seleniumhq/side-api' import noop from 'lodash/fp/noop' -import { COLOR_CYAN, vdebuglog } from 'main/util' -import getCore from '../helpers/getCore' import { Session, SessionControllerKeys } from '../../types' -const apiDebugLog = vdebuglog('api', COLOR_CYAN) - export type AsyncHandler = ( ...args: Parameters ) => Promise> @@ -52,19 +48,14 @@ const Handler = const handler = factory(path, session) const doAPI = async (...params: Parameters) => { const result = await handler(...params) - if (mutator) { - const { project, state } = mutator(getCore(session), { params, result }) - session.projects.project = project - session.state.state = state - session.api.state.onMutate.dispatchEvent(path, { params, result }) - } + await session.state.mutate(mutator, params, result, path) return result } ipcMain.handle(path, async (_event, ...args) => { - apiDebugLog('Received API Request', path, args) + session.system.loggers.api('Received API Request', path, args) const result = await doAPI(...(args as Parameters)) - apiDebugLog('Resolved API Request', path, result) - return result; + session.system.loggers.api('Resolved API Request', path, result) + return result }) return doAPI } diff --git a/packages/selenium-ide/src/main/api/classes/DriverRawHandler.ts b/packages/selenium-ide/src/main/api/classes/DriverRawHandler.ts index 2d3093f79..651c73bcf 100644 --- a/packages/selenium-ide/src/main/api/classes/DriverRawHandler.ts +++ b/packages/selenium-ide/src/main/api/classes/DriverRawHandler.ts @@ -1,13 +1,9 @@ import { Mutator } from '@seleniumhq/side-api' import { ipcMain } from 'electron' import noop from 'lodash/fp/noop' -import { COLOR_CYAN, vdebuglog } from 'main/util' import { AsyncHandler, HandlerFactory } from './Handler' -import getCore from '../helpers/getCore' import { Session, SessionControllerKeys } from '../../types' -const apiDebugLog = vdebuglog('api', COLOR_CYAN) - export type ParametersExceptFirst = F extends ( arg0: any, ...rest: infer R @@ -50,28 +46,17 @@ const Handler = const handler = factory(path, session) const doAPI = async (...params: Parameters) => { const result = await handler(...params) - if (mutator) { - const serializableParams = params.slice( - 1 - ) as ParametersExceptFirst - const newState = mutator(getCore(session), { - params: serializableParams, - result, - }) - session.projects.project = newState.project - session.state.state = newState.state - session.api.state.onMutate.dispatchEvent(path, { - params: serializableParams, - result, - }) - } + const serializableParams = params.slice( + 1 + ) as ParametersExceptFirst + await session.state.mutate(mutator, serializableParams, result, path) return result } ipcMain.handle(path, async (...args) => { - apiDebugLog('Received API Request', path, args.slice(1)) + session.system.loggers.api('Received API Request', path, args.slice(1)) const result = await doAPI(...(args as Parameters)) - apiDebugLog('Resolved API Request', path, result) - return result; + session.system.loggers.api('Resolved API Request', path, result) + return result }) return doAPI } diff --git a/packages/selenium-ide/src/main/api/classes/EventListener.ts b/packages/selenium-ide/src/main/api/classes/EventListener.ts index da5b97501..6ced5e02b 100644 --- a/packages/selenium-ide/src/main/api/classes/EventListener.ts +++ b/packages/selenium-ide/src/main/api/classes/EventListener.ts @@ -7,9 +7,6 @@ import { } from '@seleniumhq/side-api' import { Session } from 'main/types' import getCore from '../helpers/getCore' -import { COLOR_CYAN, vdebuglog } from 'main/util' - -const apiDebugLog = vdebuglog('api', COLOR_CYAN) export type MainListener< ARGS extends VariadicArgs, @@ -26,11 +23,11 @@ const baseListener = ( const listeners: any[] = [] return { addListener(listener) { - apiDebugLog('Listener added', path) + session.system.loggers.api('Listener added', path) listeners.push(listener) }, async dispatchEvent(...args) { - apiDebugLog('Dispatch event', path, args) + session.system.loggers.api('Dispatch event', path, args) if (mutator) { session.api.state.onMutate.dispatchEvent(path, args) const newState = mutator(getCore(session), args) @@ -40,7 +37,7 @@ const baseListener = ( return await Promise.all(listeners.map((fn) => fn(...args))) }, async dispatchEventAsync(...args) { - apiDebugLog('Dispatch event async', path, args) + session.system.loggers.api('Dispatch event async', path, args) if (mutator) { session.api.state.onMutate.dispatchEvent(path, args) const newState = mutator(getCore(session), args) @@ -61,7 +58,7 @@ const baseListener = ( if (index === -1) { throw new Error(`Unable to remove listener for ${path} ${listener}`) } - apiDebugLog('Listener removed', path) + session.system.loggers.api('Listener removed', path) listeners.splice(index, 1) }, } diff --git a/packages/selenium-ide/src/main/api/classes/Handler.ts b/packages/selenium-ide/src/main/api/classes/Handler.ts index c1742c6df..d933e9ab4 100644 --- a/packages/selenium-ide/src/main/api/classes/Handler.ts +++ b/packages/selenium-ide/src/main/api/classes/Handler.ts @@ -6,12 +6,8 @@ import { Mutator, } from '@seleniumhq/side-api' import noop from 'lodash/fp/noop' -import { COLOR_CYAN, vdebuglog } from 'main/util' -import getCore from '../helpers/getCore' import { Session, SessionControllerKeys } from '../../types' -const apiDebugLog = vdebuglog('api', COLOR_CYAN) - export type AsyncHandler = ( ...args: Parameters ) => Promise> @@ -31,7 +27,9 @@ const defaultHandler = ( // @ts-expect-error return controller[method].bind(controller) as AsyncHandler } - apiDebugLog(`Missing method for path ${path}, using passthrough`) + session.system.loggers.api( + `Missing method for path ${path}, using passthrough` + ) return noop as unknown as AsyncHandler } @@ -52,19 +50,14 @@ const Handler = const handler = factory(path, session) const doAPI = async (...params: Parameters) => { const result = await handler(...params) - if (mutator) { - const { project, state } = mutator(getCore(session), { params, result }) - session.projects.project = project - session.state.state = state - session.api.state.onMutate.dispatchEvent(path, { params, result }) - } + await session.state.mutate(mutator, params, result, path) return result } ipcMain.handle(path, async (_event, ...args) => { - apiDebugLog('Received API Request', path, args) + session.system.loggers.api('Received API Request', path, args) const result = await doAPI(...(args as Parameters)) - apiDebugLog('Resolved API Request', path, result) - return result; + session.system.loggers.api('Resolved API Request', path, result) + return result }) return doAPI } diff --git a/packages/selenium-ide/src/main/api/classes/RawHandler.ts b/packages/selenium-ide/src/main/api/classes/RawHandler.ts index 2d3093f79..651c73bcf 100644 --- a/packages/selenium-ide/src/main/api/classes/RawHandler.ts +++ b/packages/selenium-ide/src/main/api/classes/RawHandler.ts @@ -1,13 +1,9 @@ import { Mutator } from '@seleniumhq/side-api' import { ipcMain } from 'electron' import noop from 'lodash/fp/noop' -import { COLOR_CYAN, vdebuglog } from 'main/util' import { AsyncHandler, HandlerFactory } from './Handler' -import getCore from '../helpers/getCore' import { Session, SessionControllerKeys } from '../../types' -const apiDebugLog = vdebuglog('api', COLOR_CYAN) - export type ParametersExceptFirst = F extends ( arg0: any, ...rest: infer R @@ -50,28 +46,17 @@ const Handler = const handler = factory(path, session) const doAPI = async (...params: Parameters) => { const result = await handler(...params) - if (mutator) { - const serializableParams = params.slice( - 1 - ) as ParametersExceptFirst - const newState = mutator(getCore(session), { - params: serializableParams, - result, - }) - session.projects.project = newState.project - session.state.state = newState.state - session.api.state.onMutate.dispatchEvent(path, { - params: serializableParams, - result, - }) - } + const serializableParams = params.slice( + 1 + ) as ParametersExceptFirst + await session.state.mutate(mutator, serializableParams, result, path) return result } ipcMain.handle(path, async (...args) => { - apiDebugLog('Received API Request', path, args.slice(1)) + session.system.loggers.api('Received API Request', path, args.slice(1)) const result = await doAPI(...(args as Parameters)) - apiDebugLog('Resolved API Request', path, result) - return result; + session.system.loggers.api('Resolved API Request', path, result) + return result }) return doAPI } diff --git a/packages/selenium-ide/src/main/index.ts b/packages/selenium-ide/src/main/index.ts index a50fb3134..938726b73 100644 --- a/packages/selenium-ide/src/main/index.ts +++ b/packages/selenium-ide/src/main/index.ts @@ -13,7 +13,6 @@ app.commandLine.appendSwitch('remote-debugging-port', '8315') const log = configureLogging() autoUpdater.logger = log - // Capture and show unhandled exceptions process.on('unhandledRejection', function handleWarning(reason) { console.log('[PROCESS] Unhandled Promise Rejection') @@ -59,7 +58,7 @@ app.on('ready', async () => { app.exit(0) } }) - + app.on('window-all-closed', async () => { allWindowsClosed = true if (process.platform === 'darwin') { @@ -68,7 +67,7 @@ app.on('ready', async () => { await session.system.quit() } }) - + app.on( 'certificate-error', (event, _webContents, _url, _error, _certificate, callback) => { diff --git a/packages/selenium-ide/src/main/session/controllers/Menu/menus/application.ts b/packages/selenium-ide/src/main/session/controllers/Menu/menus/application.ts index 2fb4b8d82..32d73806f 100644 --- a/packages/selenium-ide/src/main/session/controllers/Menu/menus/application.ts +++ b/packages/selenium-ide/src/main/session/controllers/Menu/menus/application.ts @@ -36,8 +36,8 @@ export const commands: MenuComponent = (session: Session) => () => { label: '&Edit', submenu: [ - ...(editBasicsCommands(session)()), - ...(testEditorCommands(session)()), + ...editBasicsCommands(session)(), + ...testEditorCommands(session)(), ], }, { @@ -50,4 +50,4 @@ export const commands: MenuComponent = (session: Session) => () => }, ] -export default menuFactoryFromCommandFactory(commands) \ No newline at end of file +export default menuFactoryFromCommandFactory(commands) diff --git a/packages/selenium-ide/src/main/session/controllers/Menu/menus/editBasics.ts b/packages/selenium-ide/src/main/session/controllers/Menu/menus/editBasics.ts index 2b234268f..a852d7a93 100644 --- a/packages/selenium-ide/src/main/session/controllers/Menu/menus/editBasics.ts +++ b/packages/selenium-ide/src/main/session/controllers/Menu/menus/editBasics.ts @@ -1,10 +1,20 @@ -import { MenuComponent } from 'main/types' +import { MenuComponent, Session } from 'main/types' import { menuFactoryFromCommandFactory } from '../utils' -export const commands: MenuComponent = () => () => +export const commands: MenuComponent = (session: Session) => () => [ - { label: 'Undo', role: 'undo' }, - { label: 'Redo', role: 'redo' }, + { + accelerator: 'CommandOrControl+Z', + click: () => session.api.state.undo(), + enabled: session.state.prevHistory.length !== 0, + label: 'Undo', + }, + { + accelerator: 'CommandOrControl+Shift+Z', + click: () => session.api.state.redo(), + enabled: session.state.nextHistory.length !== 0, + label: 'Redo', + }, { type: 'separator' }, { label: 'Cut', role: 'cut' }, { label: 'Copy', role: 'copy' }, diff --git a/packages/selenium-ide/src/main/session/controllers/Menu/menus/projectEditor.ts b/packages/selenium-ide/src/main/session/controllers/Menu/menus/projectEditor.ts index 520143cee..25343ca02 100644 --- a/packages/selenium-ide/src/main/session/controllers/Menu/menus/projectEditor.ts +++ b/packages/selenium-ide/src/main/session/controllers/Menu/menus/projectEditor.ts @@ -26,7 +26,7 @@ export const commands: MenuComponent = (session) => () => click: async () => { await session.projects.showRecents() }, - submenu: (session.projects.getRecent()).map((project) => ({ + submenu: session.projects.getRecent().map((project) => ({ click: async () => { await session.api.projects.load(project) }, diff --git a/packages/selenium-ide/src/main/session/controllers/Menu/menus/projectView.ts b/packages/selenium-ide/src/main/session/controllers/Menu/menus/projectView.ts index c27b34c48..15916ea34 100644 --- a/packages/selenium-ide/src/main/session/controllers/Menu/menus/projectView.ts +++ b/packages/selenium-ide/src/main/session/controllers/Menu/menus/projectView.ts @@ -25,6 +25,27 @@ export const commands: MenuComponent = (session) => () => }, label: 'Reset Playback Windows', }, + { + accelerator: 'CommandOrControl+R', + label: 'Refresh Playback Window', + click: async () => { + const window = await session.windows.getActivePlaybackWindow() + if (window) { + await window.webContents.executeJavaScript('window.location.reload()') + if (session.state.state.status === 'recording') { + await session.api.recorder.recordNewCommand( + { + command: 'executeScript', + comment: 'Reload the page', + target: 'window.location.reload()', + value: '', + }, + true + ) + } + } + }, + }, ] export default menuFactoryFromCommandFactory(commands) diff --git a/packages/selenium-ide/src/main/session/controllers/Menu/menus/textField.ts b/packages/selenium-ide/src/main/session/controllers/Menu/menus/textField.ts index 6aeb15250..ab8676bc1 100644 --- a/packages/selenium-ide/src/main/session/controllers/Menu/menus/textField.ts +++ b/packages/selenium-ide/src/main/session/controllers/Menu/menus/textField.ts @@ -21,16 +21,6 @@ export const commands: MenuComponent = () => () => { type: 'separator', }, - { - accelerator: 'CommandOrControl+Z', - label: 'Undo', - role: 'undo', - }, - { - accelerator: 'Shift+CommandOrControl+Z', - label: 'Redo', - role: 'redo', - }, ] export default menuFactoryFromCommandFactory(commands) diff --git a/packages/selenium-ide/src/main/session/controllers/Playback/index.ts b/packages/selenium-ide/src/main/session/controllers/Playback/index.ts index 12bb6a0bc..6d31fc5fd 100644 --- a/packages/selenium-ide/src/main/session/controllers/Playback/index.ts +++ b/packages/selenium-ide/src/main/session/controllers/Playback/index.ts @@ -344,7 +344,9 @@ export default class PlaybackController extends BaseController { const testName = this.session.tests.getByID( testID || this.session.state.state.activeTestID )?.name - console.debug(`Playing state changed ${e.state} for test ${testName}`) + console.debug( + `Playing state changed ${e.state} for test ${testName}, doink?` + ) let closeAll = false switch (e.state) { case 'aborted': @@ -358,6 +360,13 @@ export default class PlaybackController extends BaseController { setTimeout(() => this.playNextTest(true)) } } + if (!this.playingSuite) { + if (testID && this.session.state.state.activeTestID !== testID) { + console.debug(`Setting active test to ${testID}`) + await this.session.api.state.setActiveTest(testID) + } + } + console.debug(`Releasing window for playback`) this.playbacks.splice(this.playbacks.indexOf(playback), 1) this.session.windows.releasePlaybackWindows(playback, closeAll) await playback.cleanup() @@ -367,6 +376,7 @@ export default class PlaybackController extends BaseController { if (fullyComplete) { this.playingSuite = '' } + console.debug(`Fully complete? ${fullyComplete}`) this.session.api.playback.onPlayUpdate.dispatchEvent({ state: e.state, testID, diff --git a/packages/selenium-ide/src/main/session/controllers/Polyfill/index.ts b/packages/selenium-ide/src/main/session/controllers/Polyfill/index.ts index 178380d95..41a7c444c 100644 --- a/packages/selenium-ide/src/main/session/controllers/Polyfill/index.ts +++ b/packages/selenium-ide/src/main/session/controllers/Polyfill/index.ts @@ -107,11 +107,7 @@ export default class PromptController extends BaseController { document.querySelector('#confirm').innerText = "${confirm}"; ` ) - await this.waitForResolution( - this.confirm, - successKeys, - errorKey - ) + await this.waitForResolution(this.confirm, successKeys, errorKey) this.confirm.confirm = null } @@ -237,8 +233,6 @@ export default class PromptController extends BaseController { } hasBlockingDialog() { - return ( - this.alert.ready || this.confirm.ready || this.prompt.ready || false - ) + return this.alert.ready || this.confirm.ready || this.prompt.ready || false } } diff --git a/packages/selenium-ide/src/main/session/controllers/Recorder/index.ts b/packages/selenium-ide/src/main/session/controllers/Recorder/index.ts index 149714f57..769c37018 100644 --- a/packages/selenium-ide/src/main/session/controllers/Recorder/index.ts +++ b/packages/selenium-ide/src/main/session/controllers/Recorder/index.ts @@ -4,10 +4,7 @@ import { getActiveCommandIndex, getActiveWindowHandleID, } from '@seleniumhq/side-api/dist/helpers/getActiveData' -import { - LocatorFields, - RecordNewCommandInput, -} from '@seleniumhq/side-api' +import { LocatorFields, RecordNewCommandInput } from '@seleniumhq/side-api' import { randomUUID } from 'crypto' import { relative } from 'node:path' import BaseController from '../Base' @@ -143,7 +140,9 @@ export default class RecorderController extends BaseController { * If the window was opened by a command, the handle will be the name of the window. */ async start(): Promise { - const playback = await this.session.playback.getPlayback(this.session.state.state.activeTestID) + const playback = await this.session.playback.getPlayback( + this.session.state.state.activeTestID + ) const executor = playback.executor const driver = executor.driver const useBidi = this.session.store.get('browserInfo.useBidi') diff --git a/packages/selenium-ide/src/main/session/controllers/State/index.ts b/packages/selenium-ide/src/main/session/controllers/State/index.ts index ea06afa0f..a64de9ef7 100644 --- a/packages/selenium-ide/src/main/session/controllers/State/index.ts +++ b/packages/selenium-ide/src/main/session/controllers/State/index.ts @@ -1,5 +1,5 @@ import { getCommandIndex } from '@seleniumhq/side-api/dist/helpers/getActiveData' -import { state as defaultState } from '@seleniumhq/side-api' +import { state as defaultState, Mutator } from '@seleniumhq/side-api' import { CamelCaseNamesPref, CoreSessionData, @@ -14,14 +14,59 @@ import clone from 'lodash/fp/clone' import merge from 'lodash/fp/merge' import BaseController from '../Base' import { loadingID } from '@seleniumhq/side-api/src/constants/loadingID' +import getCore from 'main/api/helpers/getCore' const queue = (op: () => void) => setTimeout(op, 0) export default class StateController extends BaseController { - static pathFromID = (id: string) => id.replace(/\-/g, '_') + static pathFromID = (id: string) => id.replace(/-/g, '_') + + prevHistory: CoreSessionData[] = [] + nextHistory: CoreSessionData[] = [] state: StateShape = clone(defaultState) + appendHistory(path: string) { + if (path.includes('setAll')) return + if (this.session.projects.project.id !== loadingID) { + this.prevHistory.push(this.get()) + this.nextHistory = [] + } + } + + async undo() { + const prev = this.prevHistory.pop() + if (prev) { + this.nextHistory.push(this.get()) + this.session.api.state.setAll(prev) + } + } + + async redo() { + const next = this.nextHistory.pop() + if (next) { + this.prevHistory.push(this.get()) + this.session.api.state.setAll(next) + } + } + + async mutate any>( + mutator: undefined | Mutator, + params: Parameters, + result: Awaited>, + path: string + ) { + if (!mutator) return + this.appendHistory(path) + const { project, state } = mutator(getCore(this.session), { + params, + result, + }) + this.session.projects.project = project + this.session.state.state = state + this.session.api.state.onMutate.dispatchEvent(path, { params, result }) + } + get(): CoreSessionData { return { project: this.session.projects.project, @@ -29,6 +74,11 @@ export default class StateController extends BaseController { } } + setAll(data: CoreSessionData) { + this.session.projects.project = data.project + this.state = data.state + } + set(key: string, _data: any) { if (key.includes('editor.overrideWindowSize')) { queue(async () => { diff --git a/packages/selenium-ide/src/main/session/controllers/System/index.ts b/packages/selenium-ide/src/main/session/controllers/System/index.ts index a99018843..ca7bdf5a5 100644 --- a/packages/selenium-ide/src/main/session/controllers/System/index.ts +++ b/packages/selenium-ide/src/main/session/controllers/System/index.ts @@ -1,6 +1,6 @@ import { dialog, ipcMain } from 'electron' import { autoUpdater } from 'electron-updater' -import { isAutomated } from 'main/util' +import { COLOR_CYAN, isAutomated, vdebuglog } from 'main/util' import { inspect } from 'util' import { writeFile } from 'fs/promises' import BaseController from '../Base' @@ -13,8 +13,12 @@ export default class SystemController extends BaseController { this.writeToLog = this.writeToLog.bind(this) } isDown = true + isDev = false shuttingDown = false logs: string[] = [] + loggers = { + api: vdebuglog('api', COLOR_CYAN), + } async dumpSession() { const response = await this.session.dialogs.openSave() @@ -47,6 +51,7 @@ export default class SystemController extends BaseController { } async startup() { + this.isDev = process.env.SIDE_DEV === '1' if (this.isDown) { // If automated, assume we already have a chromedriver process running if (!isAutomated) { @@ -141,14 +146,18 @@ export default class SystemController extends BaseController { if (confirm) { try { await this.session.driver.stopProcess() - } catch (e) {} + } catch (e) { + console.warn('Failed to stop driver process', e) + } this.isDown = true } this.shuttingDown = false } try { await this.session.api.system.onLog.removeListener(this.writeToLog) - } catch (e) {} + } catch (e) { + console.warn('Failed to remove log listener', e) + } } } diff --git a/packages/selenium-ide/src/main/session/controllers/Windows/index.ts b/packages/selenium-ide/src/main/session/controllers/Windows/index.ts index bf7805cdc..18cbb64f6 100644 --- a/packages/selenium-ide/src/main/session/controllers/Windows/index.ts +++ b/packages/selenium-ide/src/main/session/controllers/Windows/index.ts @@ -50,7 +50,7 @@ const windowLoaderFactoryMap: WindowLoaderFactoryMap = Object.fromEntries( const preloadPath = join(__dirname, `${filename}-preload-bundle.js`) const hasPreload = !filename.endsWith('-bidi') && existsSync(preloadPath) const windowLoader: WindowLoaderFactory = - (_session: Session) => + (session: Session) => (_options: BrowserWindowConstructorOptions = {}) => { const windowConfig = window() const options: Electron.BrowserWindowConstructorOptions = { @@ -66,7 +66,11 @@ const windowLoaderFactoryMap: WindowLoaderFactoryMap = Object.fromEntries( } const win = new BrowserWindow(options) - win.loadFile(join(__dirname, `${filename}.html`)) + if (session.system.isDev) { + win.loadURL(`http://localhost:8083/${filename}.html`) + } else { + win.loadFile(join(__dirname, `${filename}.html`)) + } return win } return [key, windowLoader] @@ -251,6 +255,7 @@ export default class WindowsController extends BaseController { getActivePlaybackWindow(): BrowserWindow | null { const windowCount = this.playbackWindows.length + console.log('Window count:', windowCount) if (windowCount === 0) { return null } @@ -548,6 +553,12 @@ export default class WindowsController extends BaseController { y: position[1], width: size[0], height: size[1], + webPreferences: { + ...playbackWindowOptions.webPreferences, + preload: this.session.system.isDev + ? `http://localhost:8083/playback-window-preload-bundle.js` + : join(__dirname, `playback-window-preload-bundle.js`), + }, }, } }) diff --git a/packages/selenium-ide/webpack.config.ts b/packages/selenium-ide/webpack.config.ts index 71c301037..950cb7977 100644 --- a/packages/selenium-ide/webpack.config.ts +++ b/packages/selenium-ide/webpack.config.ts @@ -18,6 +18,7 @@ import type { Configuration as DevServerConfiguration } from 'webpack-dev-server const isProduction = process.env.NODE_ENV === 'production' const isDevelopment = !isProduction +const useHMR = process.env.SIDE_DEV === '1' && isDevelopment const commonPlugins: WebpackPluginInstance[] = [ new ForkTsCheckerWebpackPlugin(), @@ -28,7 +29,7 @@ const commonPlugins: WebpackPluginInstance[] = [ if (isProduction) { commonPlugins.push(new MiniCssExtractPlugin()) } -if (isDevelopment) { +if (useHMR) { commonPlugins.push(new ReactRefreshWebpackPlugin()) } @@ -38,10 +39,6 @@ const commonConfig: Pick< > & { devServer?: DevServerConfiguration } = { - devServer: { - hot: isDevelopment, - port: 8081, - }, devtool: 'source-map', externals: ['utf-8-validate', 'bufferutil'], mode: isProduction ? 'production' : 'development', @@ -59,7 +56,7 @@ const commonConfig: Pick< loader: require.resolve('ts-loader'), options: { getCustomTransformers: () => ({ - before: [isDevelopment && ReactRefreshTypeScript()].filter(Boolean), + before: [useHMR && ReactRefreshTypeScript()].filter(Boolean), }), transpileOnly: true, }, @@ -115,10 +112,6 @@ const preloadEntries = windowData const preloadConfig: Configuration = { ...commonConfig, - devServer: { - ...commonConfig.devServer, - port: 8083, - }, entry: Object.fromEntries(preloadEntries), plugins: commonPlugins, target: 'electron-preload', @@ -134,10 +127,15 @@ const rendererEntries = windowData const rendererConfig: Configuration = { ...commonConfig, - devServer: { - ...commonConfig.devServer, - port: 8084, - }, + devServer: useHMR + ? { + devMiddleware: { + writeToDisk: true, + }, + hot: isDevelopment, + port: 8083, + } + : undefined, entry: Object.fromEntries(rendererEntries), plugins: commonPlugins.concat( Object.values(rendererEntries).map( @@ -150,10 +148,6 @@ const rendererConfig: Configuration = { const playbackPreloadBidiConfig: Configuration = { ...commonConfig, - devServer: { - ...commonConfig.devServer, - port: 8085, - }, entry: { 'playback-window-bidi-preload': path.join( __dirname, @@ -170,10 +164,6 @@ const playbackPreloadBidiConfig: Configuration = { const playbackRendererBidiConfig: Configuration = { ...commonConfig, - devServer: { - ...commonConfig.devServer, - port: 8086, - }, entry: { 'playback-window-bidi-renderer': path.join( __dirname, @@ -205,10 +195,6 @@ const playbackRendererBidiConfig: Configuration = { const mainConfig: Configuration = { ...commonConfig, - devServer: { - ...commonConfig.devServer, - port: 8087, - }, entry: { main: path.join(__dirname, 'src', 'main', 'index.ts'), }, @@ -217,11 +203,11 @@ const mainConfig: Configuration = { } export default [ - mainConfig, - preloadConfig, rendererConfig, + preloadConfig, playbackPreloadBidiConfig, playbackRendererBidiConfig, + mainConfig, ] function getBrowserPlugin(filename: string) {