Skip to content

Commit c4890ae

Browse files
feat(browser): Add support for waiting for Element to appear (#941)
1 parent 61079d3 commit c4890ae

File tree

18 files changed

+295
-3
lines changed

18 files changed

+295
-3
lines changed

extension/src/frontend/view/ElementInspector/ElementInspector.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ElementPopover } from './ElementPopover'
1919
import { AssertionEditor } from './assertions/AssertionEditor'
2020
import { AssertionData } from './assertions/types'
2121
import { useElementHighlight } from './hooks'
22+
import { WaitForData } from './waitConditions/types'
2223

2324
function getHeader(assertion: AssertionData | null) {
2425
switch (assertion?.type) {
@@ -107,6 +108,24 @@ export function ElementInspector({ onClose }: ElementInspectorProps) {
107108
setAssertion(null)
108109
}
109110

111+
const handleAddWaitFor = (data: WaitForData) => {
112+
client.send({
113+
type: 'record-events',
114+
events: [
115+
{
116+
type: 'wait-for',
117+
eventId: nanoid(),
118+
timestamp: Date.now(),
119+
tab: getTabId(),
120+
target: data.target,
121+
options: data.options,
122+
},
123+
],
124+
})
125+
126+
onClose()
127+
}
128+
110129
if (element === null) {
111130
return null
112131
}
@@ -157,7 +176,11 @@ export function ElementInspector({ onClose }: ElementInspectorProps) {
157176
onOpenChange={handleOpenChange}
158177
>
159178
{assertion === null && (
160-
<ElementMenu element={element} onSelectAssertion={setAssertion} />
179+
<ElementMenu
180+
element={element}
181+
onSelectAssertion={setAssertion}
182+
onAddWaitFor={handleAddWaitFor}
183+
/>
161184
)}
162185

163186
{assertion !== null && (

extension/src/frontend/view/ElementInspector/ElementMenu.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CheckSquareIcon,
55
EyeIcon,
66
TextCursorInputIcon,
7+
TimerIcon,
78
TypeIcon,
89
} from 'lucide-react'
910
import { ComponentProps, ReactNode } from 'react'
@@ -20,6 +21,7 @@ import {
2021
} from './ElementMenu.utils'
2122
import { AssertionData, CheckAssertionData } from './assertions/types'
2223
import { TrackedElement } from './utils'
24+
import { WaitForData } from './waitConditions/types'
2325

2426
function ToolbarRoot(props: ComponentProps<typeof Toolbar.Root>) {
2527
return (
@@ -185,9 +187,14 @@ function RoleAssertions({ role, input, onAddAssertion }: RoleCategoryProps) {
185187
interface ElementMenuProps {
186188
element: TrackedElement
187189
onSelectAssertion: (data: AssertionData) => void
190+
onAddWaitFor: (data: WaitForData) => void
188191
}
189192

190-
export function ElementMenu({ element, onSelectAssertion }: ElementMenuProps) {
193+
export function ElementMenu({
194+
element,
195+
onSelectAssertion,
196+
onAddWaitFor,
197+
}: ElementMenuProps) {
191198
const associatedElement = findAssociatedControl(element)
192199

193200
const handleAddVisibilityAssertion = () => {
@@ -206,6 +213,12 @@ export function ElementMenu({ element, onSelectAssertion }: ElementMenuProps) {
206213
})
207214
}
208215

216+
const handleAddWaitFor = () => {
217+
onAddWaitFor({
218+
target: element.target,
219+
})
220+
}
221+
209222
return (
210223
<ToolbarRoot
211224
size="1"
@@ -233,6 +246,11 @@ export function ElementMenu({ element, onSelectAssertion }: ElementMenuProps) {
233246
<TypeIcon /> <div>Add text assertion</div>
234247
</ToolbarButton>
235248
</MenuSection>
249+
<MenuSection heading="Timing">
250+
<ToolbarButton onClick={handleAddWaitFor}>
251+
<TimerIcon /> <div>Wait for element</div>
252+
</ToolbarButton>
253+
</MenuSection>
236254
</ToolbarRoot>
237255
)
238256
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { BrowserEventTarget } from '@/schemas/recording'
2+
3+
export interface WaitForData {
4+
target: BrowserEventTarget
5+
options?: {
6+
state?: 'visible' | 'hidden' | 'attached' | 'detached'
7+
timeout?: number
8+
}
9+
}

extension/src/frontend/view/ToolBox/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function ToolBox({
140140
value={tool ?? ''}
141141
onValueChange={handleToolChange}
142142
>
143-
<ToolBoxTooltip content="Pick an element to add assertions to it">
143+
<ToolBoxTooltip content="Pick an element to add assertions or wait conditions to it">
144144
<Toolbar.ToggleItem value="inspect">
145145
<SquareDashedMousePointerIcon />
146146
</Toolbar.ToggleItem>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Generated by Grafana k6 Studio (0.0.0-vitest) on 2023-10-01T00:00:00.000Z
2+
3+
import { browser } from "k6/browser";
4+
5+
export const options = {
6+
scenarios: {
7+
default: {
8+
executor: "shared-iterations",
9+
options: { browser: { type: "chromium" } },
10+
},
11+
},
12+
};
13+
14+
export default async function () {
15+
const page = await browser.newPage();
16+
17+
await page
18+
.getByTitle("Submit your form", { exact: true })
19+
.waitFor({ timeout: 5000, state: "hidden" });
20+
}

src/codegen/browser/code/options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ function isBrowserScenario(scenario: ir.Scenario) {
4343
case 'FillTextExpression':
4444
case 'CheckExpression':
4545
case 'SelectOptionsExpression':
46+
case 'WaitForExpression':
47+
case 'WaitForOptionsExpression':
4648
case 'WaitForNavigationExpression':
4749
return true
4850

src/codegen/browser/code/scenario.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
fromArrayLiteral,
1111
fromObjectLiteral,
1212
ObjectBuilder,
13+
literal,
1314
} from '@/codegen/estree'
1415
import { mapNonEmpty } from '@/utils/list'
1516
import { exhaustive } from '@/utils/typescript'
@@ -328,6 +329,41 @@ function emitExpectExpression(
328329
}
329330
}
330331

332+
function emitWaitForExpression(
333+
context: ScenarioContext,
334+
expression: ir.WaitForExpression
335+
): ts.Expression {
336+
const target = emitExpression(context, expression.target)
337+
const args =
338+
expression.options !== null
339+
? [emitExpression(context, expression.options)]
340+
: []
341+
342+
return new ExpressionBuilder(target)
343+
.member('waitFor')
344+
.call(args)
345+
.await(context)
346+
.done()
347+
}
348+
349+
function emitWaitForOptionsExpression(
350+
_context: ScenarioContext,
351+
expression: ir.WaitForOptionsExpression
352+
): ts.Expression {
353+
const timeout = typeof expression.timeout !== 'undefined' && {
354+
timeout: literal({ value: expression.timeout }),
355+
}
356+
357+
const state = typeof expression.state !== 'undefined' && {
358+
state: string(expression.state),
359+
}
360+
361+
return fromObjectLiteral({
362+
...timeout,
363+
...state,
364+
})
365+
}
366+
331367
function emitWaitForNavigationExpression(
332368
context: ScenarioContext,
333369
expression: ir.WaitForNavigationExpression
@@ -416,6 +452,12 @@ function emitExpression(
416452
case 'ExpectExpression':
417453
return emitExpectExpression(context, expression)
418454

455+
case 'WaitForExpression':
456+
return emitWaitForExpression(context, expression)
457+
458+
case 'WaitForOptionsExpression':
459+
return emitWaitForOptionsExpression(context, expression)
460+
419461
case 'WaitForNavigationExpression':
420462
return emitWaitForNavigationExpression(context, expression)
421463

src/codegen/browser/codegen.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,3 +1132,41 @@ it('should emit a getByTitle locator', async ({ expect }) => {
11321132
'__snapshots__/browser/locators/get-by-title.ts'
11331133
)
11341134
})
1135+
1136+
it('should emit a waitFor statement', async ({ expect }) => {
1137+
const script = await emitScript({
1138+
defaultScenario: {
1139+
nodes: [
1140+
{
1141+
type: 'page',
1142+
nodeId: 'page',
1143+
},
1144+
{
1145+
type: 'locator',
1146+
nodeId: 'locator',
1147+
selector: {
1148+
type: 'title',
1149+
text: 'Submit your form',
1150+
},
1151+
inputs: { page: { nodeId: 'page' } },
1152+
},
1153+
{
1154+
type: 'wait-for',
1155+
nodeId: 'wait-for',
1156+
inputs: {
1157+
locator: { nodeId: 'locator' },
1158+
},
1159+
options: {
1160+
timeout: 5000,
1161+
state: 'hidden',
1162+
},
1163+
},
1164+
],
1165+
},
1166+
scenarios: {},
1167+
})
1168+
1169+
await expect(script).toMatchFileSnapshot(
1170+
'__snapshots__/browser/wait-for-statement.ts'
1171+
)
1172+
})

src/codegen/browser/intermediate/ast.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ export interface SelectOptionsExpression {
9797
multiple: boolean
9898
}
9999

100+
export interface WaitForOptionsExpression {
101+
type: 'WaitForOptionsExpression'
102+
timeout?: number
103+
state?: 'attached' | 'detached' | 'visible' | 'hidden'
104+
}
105+
106+
export interface WaitForExpression {
107+
type: 'WaitForExpression'
108+
target: Expression
109+
options: Expression | null
110+
}
111+
100112
export interface WaitForNavigationExpression {
101113
type: 'WaitForNavigationExpression'
102114
target: Expression
@@ -178,6 +190,8 @@ export type Expression =
178190
| CheckExpression
179191
| SelectOptionsExpression
180192
| ExpectExpression
193+
| WaitForExpression
194+
| WaitForOptionsExpression
181195
| WaitForNavigationExpression
182196
| PromiseAllExpression
183197

src/codegen/browser/intermediate/context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ function buildScenarioGraph(scenario: model.Scenario) {
7575
connectPrevious(graph, node)
7676
break
7777

78+
case 'wait-for':
79+
graph.connect(node.nodeId, node.inputs.locator.nodeId, null)
80+
connectPrevious(graph, node)
81+
break
82+
7883
default:
7984
return exhaustive(node)
8085
}

0 commit comments

Comments
 (0)