Skip to content

Commit cf4a1c1

Browse files
shadowusrKuznetsovRoman
authored andcommitted
fix: enhance time travel player (#641)
* feat: fix replayer issues, add color scheme tracking, prevent player size from jumping * feat: add smooth snapshots loading, steps syncing with timeline * feat: fix fast loading animations, fix history and snapshots out of sync * fix: self-review fixes part 1 * fix: self-review fixes part 2 * fix: self-review fixes part 3 * chore: fix linter errors * fix: fix player knob border * fix: use css classes instead of inline styles * fix: use css classes instead of inline styles in player * fix: reset player state on result change * feat: handle missing snapshots in time travel * fix: enhance missing snapshot styling
1 parent 8c8c38c commit cf4a1c1

File tree

25 files changed

+1054
-141
lines changed

25 files changed

+1054
-141
lines changed

lib/adapters/event-handling/testplane/snapshots.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import yazl from 'yazl';
1010

1111
import {ReporterTestResult} from '../../test-result';
1212
import {SNAPSHOTS_PATH} from '../../../constants';
13-
import {AttachmentType, SnapshotAttachment} from '../../../types';
13+
import {AttachmentType, SnapshotAttachment, TestStepKey} from '../../../types';
1414
import {EventSource} from '../../../gui/event-source';
1515
import {ClientEvents} from '../../../gui/constants';
1616

@@ -92,7 +92,51 @@ export const finalizeSnapshotsForTest = async ({testResult, attempt, reportPath,
9292
return [];
9393
}
9494

95+
if (testResult.history && testResult.history.length > 0 && snapshots.length > 0) {
96+
const firstSnapshotTime = snapshots[0].timestamp;
97+
const lastSnapshotTime = snapshots[snapshots.length - 1].timestamp;
98+
99+
const firstHistoryTime = testResult.history[0][TestStepKey.TimeStart];
100+
const lastHistoryTime = Math.max(testResult.history[testResult.history.length - 1][TestStepKey.TimeStart], firstHistoryTime + testResult.duration);
101+
102+
if (firstHistoryTime < firstSnapshotTime) {
103+
const fakeStartSnapshot: RrwebEvent & {seqNo?: number} = {
104+
data: {id: 1, source: 3, x: 0, y: 0},
105+
timestamp: firstHistoryTime,
106+
type: 3,
107+
seqNo: -1
108+
};
109+
snapshots.unshift(fakeStartSnapshot);
110+
}
111+
112+
if (lastHistoryTime > lastSnapshotTime) {
113+
const fakeEndSnapshot: RrwebEvent & {seqNo?: number} = {
114+
data: {id: 1, source: 3, x: 0, y: 0},
115+
timestamp: lastHistoryTime,
116+
type: 3,
117+
seqNo: snapshots.length
118+
};
119+
snapshots.push(fakeEndSnapshot);
120+
}
121+
122+
snapshots.forEach((snapshot, index) => {
123+
(snapshot as RrwebEvent & {seqNo: number}).seqNo = index;
124+
});
125+
}
126+
95127
const snapshotsSerialized = snapshots.map(s => JSON.stringify(s)).join('\n');
128+
let maxWidth = 0, maxHeight = 0;
129+
for (const snapshot of snapshots) {
130+
if (snapshot.type !== 4) {
131+
continue;
132+
}
133+
if (snapshot.data.width > maxWidth) {
134+
maxWidth = snapshot.data.width;
135+
}
136+
if (snapshot.data.height > maxHeight) {
137+
maxHeight = snapshot.data.height;
138+
}
139+
}
96140

97141
const zipFilePath = createSnapshotFilePath({
98142
attempt,
@@ -114,7 +158,9 @@ export const finalizeSnapshotsForTest = async ({testResult, attempt, reportPath,
114158
zipfile.outputStream.pipe(output).on('close', () => {
115159
done([{
116160
type: AttachmentType.Snapshot,
117-
path: zipFilePath
161+
path: zipFilePath,
162+
maxWidth,
163+
maxHeight
118164
}]);
119165
});
120166

lib/static/icons/broken-snapshot.svg

Lines changed: 7 additions & 0 deletions
Loading

lib/static/modules/action-names.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export default {
5050
GROUP_TESTS_SET_CURRENT_EXPRESSION: 'GROUP_TESTS_SET_CURRENT_EXPRESSION',
5151
TOGGLE_BROWSER_CHECKBOX: 'TOGGLE_BROWSER_CHECKBOX',
5252
SUITES_PAGE_SET_CURRENT_SUITE: 'SUITES_PAGE_SET_CURRENT_SUITE',
53+
SUITES_PAGE_SET_CURRENT_STEP: 'SUITES_PAGE_SET_CURRENT_STEP',
54+
SUITES_PAGE_SET_CURRENT_HIGHLIGHT_STEP: 'SUITES_PAGE_SET_CURRENT_HIGHLIGHT_STEP',
5355
SUITES_PAGE_SET_SECTION_EXPANDED: 'SUITES_PAGE_SET_SECTION_EXPANDED',
5456
SUITES_PAGE_SET_TREE_NODE_EXPANDED: 'SUITES_PAGE_SET_TREE_NODE_EXPANDED',
5557
SUITES_PAGE_SET_ALL_TREE_NODES: 'SUITES_PAGE_SET_ALL_TREE_NODES',
@@ -67,5 +69,6 @@ export default {
6769
SORT_TESTS_SET_DIRECTION: 'SORT_TESTS_SET_DIRECTION',
6870
SET_GUI_SERVER_CONNECTION_STATUS: 'SET_GUI_SERVER_CONNECTION_STATUS',
6971
SET_AVAILABLE_FEATURES: 'SET_AVAILABLE_FEATURES',
70-
SET_SNAPSHOTS_PLAYER_HIGHLIGHT_TIME: 'SET_SNAPSHOTS_PLAYER_HIGHLIGHT_TIME'
72+
SET_SNAPSHOTS_PLAYER_HIGHLIGHT_TIME: 'SET_SNAPSHOTS_PLAYER_HIGHLIGHT_TIME',
73+
SNAPSHOTS_PLAYER_GO_TO_TIME: 'SNAPSHOTS_PLAYER_GO_TO_TIME'
7174
} as const;

lib/static/modules/actions/snapshots.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,22 @@ import actionNames from '@/static/modules/action-names';
33

44
export type SetSnapshotsPlayerHighlightTimeAction = Action<typeof actionNames.SET_SNAPSHOTS_PLAYER_HIGHLIGHT_TIME, {
55
startTime: number;
6+
endTime: number;
7+
isActive: boolean;
68
}>;
7-
export const setCurrentPlayerTime = (payload: SetSnapshotsPlayerHighlightTimeAction['payload']): SetSnapshotsPlayerHighlightTimeAction => ({
9+
export const setCurrentPlayerHighlightTime = (payload: SetSnapshotsPlayerHighlightTimeAction['payload']): SetSnapshotsPlayerHighlightTimeAction => ({
810
type: actionNames.SET_SNAPSHOTS_PLAYER_HIGHLIGHT_TIME,
911
payload
1012
});
1113

14+
export type SnapshotsPlayerGoToTimeAction = Action<typeof actionNames.SNAPSHOTS_PLAYER_GO_TO_TIME, {
15+
time: number;
16+
}>;
17+
export const goToTimeInSnapshotsPlayer = (payload: SnapshotsPlayerGoToTimeAction['payload']): SnapshotsPlayerGoToTimeAction => ({
18+
type: actionNames.SNAPSHOTS_PLAYER_GO_TO_TIME,
19+
payload
20+
});
21+
1222
export type SnapshotsAction =
13-
| SetSnapshotsPlayerHighlightTimeAction;
23+
| SetSnapshotsPlayerHighlightTimeAction
24+
| SnapshotsPlayerGoToTimeAction;

lib/static/modules/actions/suites-page.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ export const setCurrentTreeNode = (payload: SuitesPageSetCurrentTreeNodeAction['
1111
return {type: actionNames.SUITES_PAGE_SET_CURRENT_SUITE, payload};
1212
};
1313

14+
export type SuitesPageSetCurrentStepAction = Action<typeof actionNames.SUITES_PAGE_SET_CURRENT_STEP, Partial<{
15+
stepId: string | null;
16+
}>>;
17+
export const setCurrentStep = (payload: SuitesPageSetCurrentStepAction['payload']): SuitesPageSetCurrentStepAction => {
18+
return {type: actionNames.SUITES_PAGE_SET_CURRENT_STEP, payload};
19+
};
20+
21+
export type SuitesPageSetCurrentHighlightStepAction = Action<typeof actionNames.SUITES_PAGE_SET_CURRENT_HIGHLIGHT_STEP, Partial<{
22+
stepId: string | null;
23+
}>>;
24+
export const setCurrentHighlightStep = (payload: SuitesPageSetCurrentHighlightStepAction['payload']): SuitesPageSetCurrentHighlightStepAction => {
25+
return {type: actionNames.SUITES_PAGE_SET_CURRENT_HIGHLIGHT_STEP, payload};
26+
};
27+
1428
type SetTreeNodeExpandedStateAction = Action<typeof actionNames.SUITES_PAGE_SET_TREE_NODE_EXPANDED, {
1529
nodeId: string;
1630
isExpanded: boolean;
@@ -57,4 +71,6 @@ export type SuitesPageAction =
5771
| SetSectionExpandedStateAction
5872
| SetStepsExpandedStateAction
5973
| RevealTreeNodeAction
60-
| SetTreeViewModeAction;
74+
| SetTreeViewModeAction
75+
| SuitesPageSetCurrentStepAction
76+
| SuitesPageSetCurrentHighlightStepAction;

lib/static/modules/default-state.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ export default Object.assign({config: configDefaults}, {
103103
suitesPage: {
104104
currentBrowserId: null,
105105
currentTreeNodeId: null,
106-
currentGroupId: null
106+
currentGroupId: null,
107+
currentStepId: null,
108+
currentHighlightedStepId: null
107109
},
108110
visualChecksPage: {
109111
currentNamedImageId: null
@@ -130,8 +132,11 @@ export default Object.assign({config: configDefaults}, {
130132
guiServerConnection: {
131133
isConnected: false
132134
},
133-
snapshots: {
134-
currentPlayerTime: 0
135+
snapshotsPlayer: {
136+
isActive: false,
137+
highlightStartTime: 0,
138+
highlightEndTime: 0,
139+
goToTime: 0
135140
}
136141
},
137142
ui: {

lib/static/modules/reducers/features.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ export default (state: State, action: SomeAction): State => {
1010
switch (action.type) {
1111
case actionNames.INIT_GUI_REPORT:
1212
case actionNames.INIT_STATIC_REPORT: {
13-
const features: Feature[] = state.app.availableFeatures;
13+
const features: Feature[] = [...state.app.availableFeatures];
1414

1515
const toolName = action.payload.apiValues?.toolName;
1616
if (toolName === ToolName.Testplane) {
1717
features.push(ShowTimeTravelExperimentFeature);
1818

19-
const isTimeTravelAvailable = localStorageWrapper.getItem(getTimeTravelFeatureLocalStorageKey(toolName ?? ''), false);
20-
if (isTimeTravelAvailable) {
19+
const isTimeTravelEnabled = localStorageWrapper.getItem(getTimeTravelFeatureLocalStorageKey(toolName ?? ''), true);
20+
if (isTimeTravelEnabled) {
2121
features.push(TimeTravelFeature);
2222
}
2323
}

lib/static/modules/reducers/snapshots.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,20 @@ export default (state: State, action: SomeAction): State => {
88
case actionNames.SET_SNAPSHOTS_PLAYER_HIGHLIGHT_TIME: {
99
return applyStateUpdate(state, {
1010
app: {
11-
snapshots: {
12-
currentPlayerTime: action.payload.startTime
11+
snapshotsPlayer: {
12+
isActive: action.payload.isActive,
13+
highlightStartTime: action.payload.startTime,
14+
highlightEndTime: action.payload.endTime
15+
}
16+
}
17+
});
18+
}
19+
20+
case actionNames.SNAPSHOTS_PLAYER_GO_TO_TIME: {
21+
return applyStateUpdate(state, {
22+
app: {
23+
snapshotsPlayer: {
24+
goToTime: action.payload.time
1325
}
1426
}
1527
});

lib/static/modules/reducers/suites-page.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,24 @@ export default (state: State, action: SomeAction): State => {
145145
}
146146
}) as State;
147147
}
148+
case actionNames.SUITES_PAGE_SET_CURRENT_STEP: {
149+
return applyStateUpdate(state, {
150+
app: {
151+
suitesPage: {
152+
currentStepId: action.payload.stepId
153+
}
154+
}
155+
});
156+
}
157+
case actionNames.SUITES_PAGE_SET_CURRENT_HIGHLIGHT_STEP: {
158+
return applyStateUpdate(state, {
159+
app: {
160+
suitesPage: {
161+
currentHighlightedStepId: action.payload.stepId
162+
}
163+
}
164+
});
165+
}
148166
default:
149167
return state;
150168
}

lib/static/new-ui.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@
1010

1111
--color-pink-100: #fce0ff;
1212
--color-pink-600: #be00ffbf;
13+
14+
--color-neutral-50: oklch(0.985 0 0);
15+
--color-neutral-100: oklch(0.97 0 0);
16+
--color-neutral-200: oklch(0.922 0 0);
17+
--color-neutral-300: oklch(0.87 0 0);
18+
--color-neutral-400: oklch(0.708 0 0);
19+
--color-neutral-500: oklch(0.556 0 0);
20+
--color-neutral-600: oklch(0.439 0 0);
21+
--color-neutral-700: oklch(0.371 0 0);
22+
--color-neutral-800: oklch(0.269 0 0);
23+
--color-neutral-900: oklch(0.205 0 0);
24+
--color-neutral-950: oklch(0.145 0 0);
1325
}
1426

1527
.g-root {

lib/static/new-ui/components/ErrorInfo/index.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
overflow-x: scroll;
55
overflow-y: hidden;
66
background: #101827;
7-
border-radius: 5px;
7+
border-radius: 10px;
88
padding: 12px;
99
box-shadow: rgba(0, 0, 0, 0) 0 0 0 0, rgba(0, 0, 0, 0) 0 0 0 0, rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.1) 0 4px 6px -4px;
1010
}

lib/static/new-ui/components/TreeViewItem/index.module.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
--g-text-body-1-line-height: 22px;
88
--g-text-body-font-weight: normal;
99

10+
color: var(--color-title, #000);
11+
1012
line-height: 20px;
1113

1214
padding: 4px 0;
@@ -20,6 +22,15 @@
2022
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out;
2123
}
2224

25+
.tree-view-item--active {
26+
background: var(--g-color-base-brand);
27+
--g-color-base-simple-hover: var(--g-color-base-brand);
28+
--color-title: #fff;
29+
--color-icon: #fff;
30+
--color-tag-title: rgba(255, 255, 255, .7);
31+
--color-tag-bg: rgba(255, 255, 255, .15);
32+
}
33+
2334
.tree-view-item--error {
2435
--g-color-base-simple-hover: var(--g-color-private-red-50);
2536
background: var(--g-color-private-red-100);

lib/static/new-ui/components/TreeViewItem/index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ interface TreeListItemProps<T> {
2222
list: UseListResult<T>;
2323
mapItemDataToContentProps: (data: T) => ListItemViewContentType;
2424
status?: 'error' | 'corrupted';
25+
isActive?: boolean;
2526
onItemClick?: (data: {id: string}) => unknown;
2627
onMouseMove?: () => unknown;
28+
onMouseLeave?: () => unknown;
2729
}
2830

2931
export function TreeViewItem<T>(props: TreeListItemProps<T>): ReactNode {
@@ -35,9 +37,11 @@ export function TreeViewItem<T>(props: TreeListItemProps<T>): ReactNode {
3537
>
3638
<ListItemView
3739
onMouseMove={props.onMouseMove}
38-
className={classNames([styles.treeViewItem, {
39-
[styles['tree-view-item--corrupted']]: props.status === 'corrupted',
40-
[styles['tree-view-item--error']]: props.status === 'error'
40+
onMouseLeave={props.onMouseLeave}
41+
className={classNames([props.className, styles.treeViewItem, {
42+
[styles['tree-view-item--active']]: props.isActive,
43+
[styles['tree-view-item--corrupted']]: !props.isActive && props.status === 'corrupted',
44+
[styles['tree-view-item--error']]: !props.isActive && props.status === 'error'
4145
}])}
4246
activeOnHover={true}
4347
style={{'--indent': indent + Number(!hasChildren)} as React.CSSProperties}

0 commit comments

Comments
 (0)