Skip to content

Commit 4079210

Browse files
internal(browser): Create test from recording
1 parent 8367389 commit 4079210

File tree

2 files changed

+179
-0
lines changed

2 files changed

+179
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { AnyBrowserAction, ActionLocator } from '@/main/runner/schema'
2+
import {
3+
BrowserEvent,
4+
BrowserEventTarget,
5+
ElementSelector,
6+
} from '@/schemas/recording'
7+
import { exhaustive } from '@/utils/typescript'
8+
9+
function toActionLocator(selector: ElementSelector): ActionLocator {
10+
if (selector.role !== undefined) {
11+
return {
12+
type: 'role',
13+
role: selector.role.role,
14+
options: selector.role.name
15+
? { name: selector.role.name, exact: true }
16+
: undefined,
17+
}
18+
}
19+
20+
if (selector.label !== undefined) {
21+
return { type: 'label', label: selector.label }
22+
}
23+
24+
if (selector.alt !== undefined) {
25+
return { type: 'alt', text: selector.alt }
26+
}
27+
28+
if (selector.placeholder !== undefined) {
29+
return { type: 'placeholder', placeholder: selector.placeholder }
30+
}
31+
32+
if (selector.title !== undefined) {
33+
return { type: 'title', title: selector.title }
34+
}
35+
36+
if (selector.testId !== undefined && selector.testId.trim() !== '') {
37+
return { type: 'testid', testId: selector.testId }
38+
}
39+
40+
return { type: 'css', selector: selector.css }
41+
}
42+
43+
function getLocator(target: BrowserEventTarget): ActionLocator {
44+
return toActionLocator(target.selectors)
45+
}
46+
47+
function toClickModifiers(modifiers: {
48+
ctrl: boolean
49+
shift: boolean
50+
alt: boolean
51+
meta: boolean
52+
}): ('Alt' | 'Control' | 'Meta' | 'Shift')[] {
53+
const result: ('Alt' | 'Control' | 'Meta' | 'Shift')[] = []
54+
if (modifiers.ctrl) result.push('Control')
55+
if (modifiers.shift) result.push('Shift')
56+
if (modifiers.alt) result.push('Alt')
57+
if (modifiers.meta) result.push('Meta')
58+
return result
59+
}
60+
61+
function toAction(event: BrowserEvent): AnyBrowserAction | null {
62+
switch (event.type) {
63+
case 'navigate-to-page':
64+
if (event.source === 'implicit') {
65+
return null
66+
}
67+
return { method: 'page.goto', url: event.url }
68+
69+
case 'reload-page':
70+
return { method: 'page.reload' }
71+
72+
case 'click': {
73+
const clickModifiers = toClickModifiers(event.modifiers)
74+
const hasNonDefaultOptions =
75+
event.button !== 'left' || clickModifiers.length > 0
76+
77+
return {
78+
method: 'locator.click',
79+
locator: getLocator(event.target),
80+
...(hasNonDefaultOptions && {
81+
options: {
82+
button: event.button,
83+
modifiers: clickModifiers,
84+
},
85+
}),
86+
}
87+
}
88+
89+
case 'input-change':
90+
return {
91+
method: 'locator.fill',
92+
locator: getLocator(event.target),
93+
value: event.value,
94+
}
95+
96+
case 'check-change':
97+
return event.checked
98+
? { method: 'locator.check', locator: getLocator(event.target) }
99+
: { method: 'locator.uncheck', locator: getLocator(event.target) }
100+
101+
case 'radio-change':
102+
return {
103+
method: 'locator.check',
104+
locator: getLocator(event.target),
105+
}
106+
107+
case 'select-change':
108+
return {
109+
method: 'locator.selectOption',
110+
locator: getLocator(event.target),
111+
values: event.selected.map((value) => ({ value })),
112+
}
113+
114+
case 'submit-form':
115+
return {
116+
method: 'locator.click',
117+
locator: getLocator(event.submitter),
118+
}
119+
120+
case 'assert':
121+
// TODO: Add assertion support
122+
return null
123+
124+
case 'wait-for':
125+
return {
126+
method: 'locator.waitFor',
127+
locator: getLocator(event.target),
128+
options: event.options,
129+
}
130+
131+
default:
132+
return exhaustive(event)
133+
}
134+
}
135+
136+
export function convertEventsToActions(
137+
events: BrowserEvent[]
138+
): AnyBrowserAction[] {
139+
const actions: AnyBrowserAction[] = []
140+
141+
for (const event of events) {
142+
const action = toAction(event)
143+
if (action !== null) {
144+
actions.push(action)
145+
}
146+
}
147+
148+
return actions
149+
}

src/views/RecordingPreviewer/RecordingPreviewerControls.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { css } from '@emotion/react'
22
import { Button, DropdownMenu, Flex, IconButton, Text } from '@radix-ui/themes'
3+
import log from 'electron-log/renderer'
34
import { ChevronDownIcon, EllipsisVerticalIcon } from 'lucide-react'
45
import { useState } from 'react'
56
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
67

78
import { emitScript } from '@/codegen/browser'
9+
import { convertEventsToActions } from '@/codegen/browser/convertEventsToActions'
810
import { convertEventsToTest } from '@/codegen/browser/test'
911
import { DeleteFileDialog } from '@/components/DeleteFileDialog'
1012
import { useCreateGenerator } from '@/hooks/useCreateGenerator'
@@ -55,6 +57,28 @@ export function RecordingPreviewControls({
5557
navigate(getRoutePath('home'))
5658
}
5759

60+
const handleCreateBrowserTest = async () => {
61+
try {
62+
const actions = convertEventsToActions(browserEvents)
63+
const newFileName = await window.studio.browserTest.create()
64+
await window.studio.browserTest.save(newFileName, {
65+
version: '1.0',
66+
actions,
67+
})
68+
navigate(
69+
getRoutePath('browserTestEditor', {
70+
fileName: encodeURIComponent(newFileName),
71+
})
72+
)
73+
} catch (err) {
74+
log.error(err)
75+
showToast({
76+
title: 'Failed to create browser test.',
77+
status: 'error',
78+
})
79+
}
80+
}
81+
5882
const handleExportBrowserScript = (fileName: string) => {
5983
const test = convertEventsToTest({
6084
browserEvents,
@@ -112,6 +136,12 @@ export function RecordingPreviewControls({
112136
/>
113137
<MenuItem
114138
label="Browser test"
139+
description="Create a browser test from recorded interactions"
140+
disabled={browserEvents.length === 0}
141+
onClick={handleCreateBrowserTest}
142+
/>
143+
<MenuItem
144+
label="Browser script"
115145
description="Export a k6 script simulating browser interactions"
116146
disabled={browserEvents.length === 0}
117147
onClick={() => setShowExportDialog(true)}

0 commit comments

Comments
 (0)