Skip to content

Commit 926dcba

Browse files
committed
fix(replay): fix command overlays
feat(replay): add keyboard control to playbar for fine grained navigation fix(replay): fix unresponsive window dialog
1 parent 80ed2a1 commit 926dcba

File tree

23 files changed

+230
-187
lines changed

23 files changed

+230
-187
lines changed

replay-app/backend/Application.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ export default class Application {
188188
this.overlayManager.show(name, browserWindow, rect, ...args);
189189
});
190190

191+
ipcMain.on('message-overlay:hide', (e, webContentsId, messageId) => {
192+
this.overlayManager.getByWebContentsId(webContentsId).hide();
193+
Window.current.hideMessageOverlay(messageId);
194+
});
195+
191196
ipcMain.on('overlay:hide', (e, webContentsId) => {
192197
this.overlayManager.getByWebContentsId(webContentsId).hide();
193198
});

replay-app/backend/api/ReplayTabState.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ export default class ReplayTabState extends EventEmitter {
7171
return this.ticks.find(x => x.commandId === pageToLoad.commandId)?.playbarOffsetPercent ?? 0;
7272
}
7373

74+
public gotoPreviousTick() {
75+
const prevTickIdx = this.currentTickIdx > 0 ? this.currentTickIdx - 1 : this.currentTickIdx;
76+
return this.loadTick(prevTickIdx);
77+
}
78+
7479
public gotoNextTick() {
7580
const result = this.loadTick(this.currentTickIdx + 1);
7681
if (
@@ -147,7 +152,7 @@ export default class ReplayTabState extends EventEmitter {
147152
}
148153

149154
console.log(
150-
'Paint load. Current=%s. New: %s->%s (%s, back? %s)',
155+
'Paint load. Current=%s. New: %s->%s (paints: %s, back? %s)',
151156
this.paintEventsLoadedIdx,
152157
startIndex,
153158
newTick.paintEventIdx,
@@ -318,15 +323,16 @@ export default class ReplayTabState extends EventEmitter {
318323
if (command.resultNodeIds) {
319324
lastSelectedNodeIds = command.resultNodeIds;
320325
}
321-
if (lastFocusEventIdx !== null) {
322-
const focusEvent = this.focusEvents[lastFocusEventIdx];
323-
if (focusEvent.event === 0 && !lastSelectedNodeIds) {
324-
lastSelectedNodeIds = [focusEvent.targetNodeId];
325-
}
326-
}
327326
break;
328327
case 'focus':
329328
lastFocusEventIdx = tick.eventTypeIdx;
329+
const focusEvent = this.focusEvents[lastFocusEventIdx];
330+
if (focusEvent.event === 0 && focusEvent.targetNodeId) {
331+
lastSelectedNodeIds = [focusEvent.targetNodeId];
332+
} else if (focusEvent.event === 1) {
333+
lastSelectedNodeIds = null;
334+
}
335+
330336
break;
331337
case 'paint':
332338
lastPaintEventIdx = tick.eventTypeIdx;
@@ -336,6 +342,12 @@ export default class ReplayTabState extends EventEmitter {
336342
break;
337343
case 'mouse':
338344
lastMouseEventIdx = tick.eventTypeIdx;
345+
const mouseEvent = this.mouseEvents[lastMouseEventIdx];
346+
if (mouseEvent.event === 1 && mouseEvent.targetNodeId) {
347+
lastSelectedNodeIds = [mouseEvent.targetNodeId];
348+
} else if (mouseEvent.event === 2) {
349+
lastSelectedNodeIds = null;
350+
}
339351
break;
340352
}
341353

@@ -353,6 +365,7 @@ export default class ReplayTabState extends EventEmitter {
353365
tick.paintEventIdx = -1;
354366
}
355367
}
368+
this.checkBroadcast();
356369
}
357370

358371
public checkBroadcast() {

replay-app/backend/api/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export default class ReplayApi {
1818
public readonly saSession: ISaSession;
1919
public tabs: ReplayTabState[] = [];
2020
public apiHost: string;
21-
public unresponsiveSeconds = 0;
21+
public lastActivityDate: Date;
22+
public lastCommandName: string;
23+
public showUnresponsiveMessage = true;
2224

2325
public onNewTab?: (tab: ReplayTabState) => any;
2426

@@ -131,7 +133,8 @@ export default class ReplayApi {
131133
console.log('ScriptState', data);
132134
const closeDate = data.closeDate ? new Date(data.closeDate) : null;
133135
this.replayTime.update(closeDate);
134-
this.unresponsiveSeconds = data.unresponsiveSeconds;
136+
this.lastActivityDate = data.lastActivityDate ? new Date(data.lastActivityDate) : null;
137+
this.lastCommandName = data.lastCommandName;
135138
for (const tab of this.tabs) tabsWithChanges.add(tab);
136139
} else {
137140
for (const event of data) {

replay-app/backend/managers/OverlayManager.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { BrowserWindow } from "electron";
2-
import BaseOverlay from "../overlays/BaseOverlay";
3-
import MainMenu from "../overlays/MainMenu";
4-
import LocationsMenu from "../overlays/LocationsMenu";
5-
import IRectangle from "~shared/interfaces/IRectangle";
6-
import CommandOverlay from "../overlays/CommandOverlay";
7-
import MessageOverlay from "../overlays/MessageOverlay";
8-
import ListMenu from "~backend/overlays/ListMenu";
1+
import { BrowserWindow } from 'electron';
2+
import BaseOverlay from '../overlays/BaseOverlay';
3+
import MainMenu from '../overlays/MainMenu';
4+
import LocationsMenu from '../overlays/LocationsMenu';
5+
import IRectangle from '~shared/interfaces/IRectangle';
6+
import CommandOverlay from '../overlays/CommandOverlay';
7+
import MessageOverlay from '../overlays/MessageOverlay';
8+
import ListMenu from '~backend/overlays/ListMenu';
99

1010
export default class OverlayManager {
1111
private overlays: BaseOverlay[] = [];

replay-app/backend/menus/generateAppMenu.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { app, BrowserWindow, ipcMain, Menu, MenuItem, webContents } from "electron";
2-
import { saveAs, viewSource } from "./CommonActions";
3-
import Window from "../models/Window";
1+
import { app, BrowserWindow, ipcMain, Menu, MenuItem, webContents } from 'electron';
2+
import { saveAs, viewSource } from './CommonActions';
3+
import Window from '../models/Window';
44

55
const isMac = process.platform === 'darwin';
66

@@ -126,6 +126,25 @@ export default function generateAppMenu() {
126126
),
127127
],
128128
},
129+
{
130+
label: 'Replay',
131+
submenu: [
132+
{
133+
label: 'View History',
134+
click: () => {
135+
return Window.current.openAppLocation('History');
136+
},
137+
},
138+
...createMenuItem(['Left'], () => {
139+
if (!Window.current) return;
140+
return Window.current.replayView?.gotoPreviousTick();
141+
}),
142+
...createMenuItem(['Right'], () => {
143+
if (!Window.current) return;
144+
return Window.current.replayView?.gotoNextTick();
145+
}),
146+
],
147+
},
129148
{
130149
label: 'Tools',
131150
submenu: [
@@ -177,23 +196,6 @@ export default function generateAppMenu() {
177196
},
178197
];
179198

180-
// Ctrl+1 - Ctrl+8
181-
template[0].submenu = template[0].submenu.concat(
182-
createMenuItem(
183-
Array.from({ length: 8 }, (v, k) => k + 1).map(i => `CmdOrCtrl+${i}`),
184-
(window, menuItem, i) => {
185-
Window.current.webContents.send('select-tab-index', i);
186-
},
187-
),
188-
);
189-
190-
// Ctrl+9
191-
template[0].submenu = template[0].submenu.concat(
192-
createMenuItem(['CmdOrCtrl+9'], () => {
193-
Window.current.webContents.send('select-last-tab');
194-
}),
195-
);
196-
197199
return Menu.buildFromTemplate(template);
198200
}
199201

replay-app/backend/menus/generateContextMenu.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { clipboard, Menu, nativeImage } from 'electron';
22
import { printPage, saveAs, viewSource } from './CommonActions';
3+
import Window from '../models/Window';
34

45
export default function generateContextMenu(
56
params: Electron.ContextMenuParams,
@@ -109,17 +110,17 @@ export default function generateContextMenu(
109110
{
110111
label: 'Go back',
111112
accelerator: 'Alt+Left',
112-
enabled: webContents.canGoBack(),
113+
enabled: Window.current.hasBack(),
113114
click: () => {
114-
webContents.goBack();
115+
return Window.current.goBack();
115116
},
116117
},
117118
{
118119
label: 'Go forward',
119120
accelerator: 'Alt+Right',
120-
enabled: webContents.canGoForward(),
121+
enabled: Window.current.hasNext(),
121122
click: () => {
122-
webContents.goForward();
123+
return Window.current.goForward();
123124
},
124125
},
125126
{

replay-app/backend/models/PlaybarView.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import IRectangle from '~shared/interfaces/IRectangle';
44
import ReplayTabState from '~backend/api/ReplayTabState';
55
import ViewBackend from '~backend/models/ViewBackend';
66

7+
78
export default class PlaybarView extends ViewBackend {
89
private readonly isReady: Promise<void>;
910
private tabState: ReplayTabState;
@@ -55,7 +56,12 @@ export default class PlaybarView extends ViewBackend {
5556
}
5657
}
5758

58-
public async changeTickOffset(offset: number) {
59+
public pause() {
60+
this.playOnLoaded = false;
61+
this.browserView.webContents.send('pause');
62+
}
63+
64+
public changeTickOffset(offset: number) {
5965
this.browserView.webContents.send('ticks:change-offset', offset);
6066
}
6167

@@ -64,7 +70,8 @@ export default class PlaybarView extends ViewBackend {
6470
const tick = this.tabState.ticks.find(x => x.playbarOffsetPercent === tickValue);
6571
if (!tick) return;
6672

67-
rect.y += this.bounds.y;
73+
const bounds = this.browserView.getBounds();
74+
rect.y += bounds.y - bounds.height;
6875
const commandLabel = tick.label;
6976
const commandResult =
7077
tick.eventType === 'command'

replay-app/backend/models/ReplayView.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import { IMouseEvent, IScrollRecord } from '~shared/interfaces/ISaSession';
1313
const domReplayerScript = require.resolve('../../injected-scripts/domReplayerSubscribe');
1414

1515
export default class ReplayView extends ViewBackend {
16+
public static MESSAGE_HANG_ID = 'script-hang';
1617
public replayApi: ReplayApi;
1718
public tabState: ReplayTabState;
1819
public readonly playbarView: PlaybarView;
1920

21+
private lastInactivityMillis = 0;
22+
2023
public constructor(window: Window) {
2124
super(window, {
2225
preload: domReplayerScript,
@@ -25,6 +28,7 @@ export default class ReplayView extends ViewBackend {
2528
contextIsolation: true,
2629
javascript: false,
2730
});
31+
2832
this.interceptHttpRequests();
2933
this.playbarView = new PlaybarView(window);
3034
this.checkResponsive = this.checkResponsive.bind(this);
@@ -102,10 +106,22 @@ export default class ReplayView extends ViewBackend {
102106
await this.publishTickChanges(events);
103107
}
104108

109+
public async gotoNextTick() {
110+
const offset = await this.nextTick();
111+
this.playbarView.changeTickOffset(offset);
112+
}
113+
114+
public async gotoPreviousTick() {
115+
const state = this.tabState.gotoPreviousTick();
116+
await this.publishTickChanges(state);
117+
this.playbarView.changeTickOffset(this.tabState.currentPlaybarOffsetPct);
118+
}
119+
105120
public async nextTick() {
106121
if (!this.isAttached || !this.tabState) return 0;
107122
const events = this.tabState.gotoNextTick();
108123
await this.publishTickChanges(events);
124+
setImmediate(() => this.checkResponsive());
109125

110126
return this.tabState.currentPlaybarOffsetPct;
111127
}
@@ -123,9 +139,13 @@ export default class ReplayView extends ViewBackend {
123139
const [domChanges] = events;
124140
if (domChanges?.length) {
125141
if (domChanges[0].action === 'newDocument') {
126-
const nav = domChanges.shift();
127-
console.log('Re-navigating', nav.textContent);
128-
await this.webContents.loadURL(nav.textContent);
142+
const url = new URL(domChanges[0].textContent);
143+
const currentUrl = new URL(this.webContents.getURL());
144+
if (url.origin !== currentUrl.origin) {
145+
console.log('Re-navigating', url.href);
146+
domChanges.shift();
147+
await this.webContents.loadURL(url.href);
148+
}
129149
}
130150
}
131151
this.webContents.send('dom:apply', ...events);
@@ -142,14 +162,30 @@ export default class ReplayView extends ViewBackend {
142162
}
143163

144164
private checkResponsive() {
145-
if (this.replayApi.unresponsiveSeconds >= 5) {
165+
const lastActivityMillis =
166+
new Date().getTime() - (this.replayApi.lastActivityDate ?? new Date()).getTime();
167+
168+
if (lastActivityMillis < this.lastInactivityMillis) {
169+
this.lastInactivityMillis = lastActivityMillis;
170+
return;
171+
}
172+
if (lastActivityMillis - this.lastInactivityMillis < 500) return;
173+
this.lastInactivityMillis = lastActivityMillis;
174+
175+
if (
176+
lastActivityMillis >= 5e3 &&
177+
this.replayApi.lastCommandName !== 'waitForMillis' &&
178+
this.replayApi.showUnresponsiveMessage
179+
) {
180+
const lastActivitySecs = Math.floor(lastActivityMillis / 1e3);
146181
Application.instance.overlayManager.show(
147182
'message-overlay',
148183
this.window.browserWindow,
149184
this.window.browserWindow.getContentBounds(),
150185
{
151186
title: 'Did your script hang?',
152-
message: `The last update was ${this.replayApi.unresponsiveSeconds} seconds ago.`,
187+
message: `The last update was ${lastActivitySecs} seconds ago.`,
188+
id: ReplayView.MESSAGE_HANG_ID,
153189
},
154190
);
155191
} else {

replay-app/backend/models/Window.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ export default class Window {
9999
}
100100
}
101101

102+
public hasBack() {
103+
return this.navCursor > 0;
104+
}
105+
106+
public hasNext() {
107+
return this.navCursor + 1 < this.navHistory.length;
108+
}
109+
102110
public async openAppLocation(location: IWindowLocation, navigateToHistoryIdx?: number) {
103111
console.log('Navigating to %s', location);
104112

@@ -173,6 +181,12 @@ export default class Window {
173181
}
174182
}
175183

184+
public hideMessageOverlay(messageId: string) {
185+
if (messageId === ReplayView.MESSAGE_HANG_ID) {
186+
this.replayView.replayApi.showUnresponsiveMessage = false;
187+
}
188+
}
189+
176190
protected async load(state: { replayApi?: ReplayApi; location?: IWindowLocation }) {
177191
const { replayApi, location } = state || {};
178192
await this.browserWindow.loadURL(Application.instance.getPageUrl('header'));
@@ -203,22 +217,14 @@ export default class Window {
203217
return this.openReplayApi(api, index);
204218
}
205219

206-
private hasBack() {
207-
return this.navCursor > 0;
208-
}
209-
210-
private hasNext() {
211-
return this.navCursor + 1 < this.navHistory.length;
212-
}
213-
214220
private logHistory(
215221
history: { replayMeta?: IReplayMeta; location?: IWindowLocation },
216222
historyIdx?: number,
217223
) {
218224
if (historyIdx !== undefined) {
219225
this.navCursor = historyIdx;
220226
} else {
221-
this.navHistory.length = this.navCursor+1;
227+
this.navHistory.length = this.navCursor + 1;
222228
this.navCursor = this.navHistory.length;
223229
this.navHistory.push(history);
224230
}

0 commit comments

Comments
 (0)