Skip to content

Commit efa3b16

Browse files
internal(browser): Add wait for element action (#1093)
1 parent 8134c9a commit efa3b16

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2006
-767
lines changed

src/codegen/browser/__snapshots__/browser/locators/get-by-role.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default async function () {
1515
const page = await browser.newPage();
1616

1717
try {
18-
await page.getByRole("button", { name: "Submit", exact: true }).click();
18+
await page.getByRole("button", { name: "Submit" }).click();
1919
} finally {
2020
await page?.close();
2121
}

src/codegen/browser/code/options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function isBrowserScenario(scenario: ir.Scenario) {
3636
case 'GotoExpression':
3737
case 'ReloadExpression':
3838
case 'NewRoleLocatorExpression':
39+
case 'RoleLocatorOptionsExpression':
3940
case 'NewLabelLocatorExpression':
4041
case 'NewCssLocatorExpression':
4142
case 'NewAltTextLocatorExpression':

src/codegen/browser/code/scenario.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,35 @@ function emitNewRoleLocatorExpression(
5656
): ts.Expression {
5757
const page = emitExpression(context, expression.page)
5858
const role = emitExpression(context, expression.role)
59-
const name = emitExpression(context, expression.name)
59+
60+
const options =
61+
expression.options !== null
62+
? [emitExpression(context, expression.options)]
63+
: []
6064

6165
return new ExpressionBuilder(page)
6266
.member('getByRole')
63-
.call([role, fromObjectLiteral({ name, exact: true })])
67+
.call([role, ...options])
6468
.done()
6569
}
6670

71+
function emitRoleLocatorOptionsExpression(
72+
context: ScenarioContext,
73+
expression: ir.RoleLocatorOptionsExpression
74+
): ts.Expression {
75+
if (!expression.name) {
76+
return emitExpression(context, { type: 'NullLiteral' })
77+
}
78+
79+
const name = expression.name.value
80+
const exact = expression.name.exact
81+
82+
return ObjectBuilder.from({
83+
...(name && { name }),
84+
...(exact && { exact }),
85+
})
86+
}
87+
6788
function emitNewLabelLocatorExpression(
6889
context: ScenarioContext,
6990
expression: ir.NewLabelLocatorExpression
@@ -433,6 +454,9 @@ function emitExpression(
433454
case 'NewRoleLocatorExpression':
434455
return emitNewRoleLocatorExpression(context, expression)
435456

457+
case 'RoleLocatorOptionsExpression':
458+
return emitRoleLocatorOptionsExpression(context, expression)
459+
436460
case 'NewLabelLocatorExpression':
437461
return emitNewLabelLocatorExpression(context, expression)
438462

src/codegen/browser/codegen.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,7 @@ it('should emit a getByRole locator', async ({ expect }) => {
962962
selector: {
963963
type: 'role',
964964
role: 'button',
965-
name: 'Submit',
965+
name: { value: 'Submit' },
966966
},
967967
inputs: { page: { nodeId: 'page' } },
968968
},

src/codegen/browser/intermediate/ast.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,19 @@ export interface NewTitleLocatorExpression {
5757
page: Expression
5858
}
5959

60+
export interface RoleLocatorOptionsExpression {
61+
type: 'RoleLocatorOptionsExpression'
62+
name?: {
63+
value: string
64+
exact?: boolean
65+
}
66+
}
67+
6068
export interface NewRoleLocatorExpression {
6169
type: 'NewRoleLocatorExpression'
6270
role: Expression
63-
name: Expression
6471
page: Expression
72+
options: Expression | null
6573
}
6674

6775
export interface GotoExpression {
@@ -187,6 +195,7 @@ export type Expression =
187195
| NewPageExpression
188196
| ClosePageExpression
189197
| NewRoleLocatorExpression
198+
| RoleLocatorOptionsExpression
190199
| NewLabelLocatorExpression
191200
| NewPlaceholderLocatorExpression
192201
| NewTitleLocatorExpression

src/codegen/browser/intermediate/index.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,18 @@ function emitLocatorNode(context: IntermediateContext, node: m.LocatorNode) {
7070
type: 'StringLiteral',
7171
value: node.selector.role,
7272
},
73-
name: {
74-
type: 'StringLiteral',
75-
// getByRole creates an internal selector, e.g. internal:role=link[name='Hello's] that is passed
76-
// to the browser. Since the string literal value is wrapped in single quotes, we need to escape
77-
// any single quotes in the name. Bug report: https://github.com/grafana/k6/issues/5360
78-
value: node.selector.name.replaceAll("'", "\\'"),
79-
},
73+
options: node.selector.name
74+
? {
75+
type: 'RoleLocatorOptionsExpression',
76+
name: {
77+
// getByRole creates an internal selector, e.g. internal:role=link[name='Hello's] that is passed
78+
// to the browser. Since the string literal value is wrapped in single quotes, we need to escape
79+
// any single quotes in the name. Bug report: https://github.com/grafana/k6/issues/5360
80+
value: node.selector.name.value.replaceAll("'", "\\'"),
81+
exact: node.selector.name.exact || undefined,
82+
},
83+
}
84+
: null,
8085
page,
8186
})
8287
break

src/codegen/browser/intermediate/variables.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ function substituteExpression(
5353
case 'NullLiteral':
5454
case 'NewPageExpression':
5555
case 'ClickOptionsExpression':
56+
case 'WaitForOptionsExpression':
57+
case 'RoleLocatorOptionsExpression':
5658
return node
5759

5860
case 'ClosePageExpression':
@@ -71,8 +73,10 @@ function substituteExpression(
7173
return {
7274
type: 'NewRoleLocatorExpression',
7375
role: substituteExpression(node.role, substitutions),
74-
name: substituteExpression(node.name, substitutions),
7576
page: substituteExpression(node.page, substitutions),
77+
options: node.options
78+
? substituteExpression(node.options, substitutions)
79+
: null,
7680
}
7781

7882
case 'NewLabelLocatorExpression':
@@ -187,9 +191,6 @@ function substituteExpression(
187191
: null,
188192
}
189193

190-
case 'WaitForOptionsExpression':
191-
return node
192-
193194
case 'WaitForNavigationExpression':
194195
return {
195196
type: 'WaitForNavigationExpression',

src/codegen/browser/selectors.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ActionLocator } from '@/main/runner/schema'
12
import { ElementSelector } from '@/schemas/recording'
23
import {
34
GetByAltTextNodeSelector,
@@ -21,7 +22,9 @@ function getRoleSelector(
2122
return {
2223
type: 'role',
2324
role: selectors.role.role,
24-
name: selectors.role.name,
25+
name: selectors.role.name
26+
? { value: selectors.role.name, exact: true }
27+
: undefined,
2528
}
2629
}
2730

@@ -109,6 +112,67 @@ export function getNodeSelector(selector: ElementSelector): NodeSelector {
109112
)
110113
}
111114

115+
export function toNodeSelector(locator: ActionLocator): NodeSelector {
116+
switch (locator.type) {
117+
case 'css':
118+
return {
119+
type: 'css',
120+
selector: locator.selector,
121+
}
122+
123+
case 'role':
124+
return {
125+
type: 'role',
126+
role: locator.role,
127+
name: locator.options?.name
128+
? {
129+
value: locator.options.name,
130+
exact: locator.options.exact,
131+
}
132+
: undefined,
133+
}
134+
135+
case 'testid':
136+
return {
137+
type: 'test-id',
138+
testId: locator.testId,
139+
}
140+
141+
case 'alt':
142+
return {
143+
type: 'alt',
144+
text: locator.text,
145+
}
146+
147+
case 'label':
148+
return {
149+
type: 'label',
150+
text: locator.label,
151+
}
152+
153+
case 'placeholder':
154+
return {
155+
type: 'placeholder',
156+
text: locator.placeholder,
157+
}
158+
159+
case 'title':
160+
return {
161+
type: 'title',
162+
text: locator.title,
163+
}
164+
165+
case 'text':
166+
return {
167+
type: 'text',
168+
text: locator.text,
169+
}
170+
171+
default:
172+
return exhaustive(locator)
173+
}
174+
}
175+
112176
export function isSelectorEqual(a: NodeSelector, b: NodeSelector): boolean {
113177
switch (a.type) {
114178
case 'css':
@@ -118,7 +182,12 @@ export function isSelectorEqual(a: NodeSelector, b: NodeSelector): boolean {
118182
return b.type === 'test-id' && a.testId === b.testId
119183

120184
case 'role':
121-
return b.type === 'role' && a.role === b.role && a.name === b.name
185+
return (
186+
b.type === 'role' &&
187+
a.role === b.role &&
188+
a.name?.value === b.name?.value &&
189+
a.name?.exact === b.name?.exact
190+
)
122191

123192
case 'alt':
124193
return b.type === 'alt' && a.text === b.text

src/codegen/browser/test.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { AnyBrowserAction } from '@/main/runner/schema'
21
import {
32
Assertion,
43
BrowserEvent,
54
BrowserEventTarget,
65
} from '@/schemas/recording'
76
import { exhaustive } from '@/utils/typescript'
7+
import {
8+
BrowserActionInstance,
9+
LocatorOptions,
10+
} from '@/views/BrowserTestEditor/types'
811

9-
import { isSelectorEqual, getNodeSelector } from './selectors'
12+
import { isSelectorEqual, getNodeSelector, toNodeSelector } from './selectors'
1013
import {
1114
TestNode,
1215
PageNode,
@@ -287,8 +290,11 @@ function buildBrowserNodeGraphFromEvents(events: BrowserEvent[]) {
287290
return nodes
288291
}
289292

290-
function buildBrowserNodeGraphFromActions(browserActions: AnyBrowserAction[]) {
293+
function buildBrowserNodeGraphFromActions(
294+
browserActions: BrowserActionInstance[]
295+
) {
291296
const nodes: TestNode[] = []
297+
let previousLocatorNode: LocatorNode | null = null
292298

293299
let currentPage: PageNode | undefined = undefined
294300

@@ -307,7 +313,46 @@ function buildBrowserNodeGraphFromActions(browserActions: AnyBrowserAction[]) {
307313
return toNodeRef(currentPage)
308314
}
309315

310-
function toNode(action: AnyBrowserAction): TestNode {
316+
function getLocator({ current, values }: LocatorOptions): NodeRef {
317+
const currentLocator = values[current]
318+
if (!currentLocator) {
319+
throw new Error(
320+
`Current locator of type "${current}" not found in locator values.`
321+
)
322+
}
323+
324+
// Group sequential locators together, so that we reuse the same locator
325+
// multiple actions have occurred on the same element, e.g:
326+
// ```
327+
// const input = page.locator("input")
328+
//
329+
// await input.focus()
330+
// await input.type("Hello")
331+
// await input.press("Enter")
332+
333+
const selector = toNodeSelector(currentLocator)
334+
335+
if (
336+
previousLocatorNode === null ||
337+
!isSelectorEqual(selector, previousLocatorNode.selector) ||
338+
previousLocatorNode.inputs.page.nodeId !== getPage().nodeId
339+
) {
340+
previousLocatorNode = {
341+
type: 'locator',
342+
nodeId: crypto.randomUUID(),
343+
selector,
344+
inputs: {
345+
page: getPage(),
346+
},
347+
}
348+
349+
nodes.push(previousLocatorNode)
350+
}
351+
352+
return toNodeRef(previousLocatorNode)
353+
}
354+
355+
function toNode(action: BrowserActionInstance): TestNode {
311356
switch (action.method) {
312357
case 'page.goto':
313358
return {
@@ -320,6 +365,22 @@ function buildBrowserNodeGraphFromActions(browserActions: AnyBrowserAction[]) {
320365
},
321366
}
322367
case 'page.reload':
368+
return {
369+
type: 'reload',
370+
nodeId: crypto.randomUUID(),
371+
inputs: {
372+
page: getPage(),
373+
},
374+
}
375+
case 'locator.waitFor':
376+
return {
377+
type: 'wait-for',
378+
nodeId: crypto.randomUUID(),
379+
inputs: {
380+
locator: getLocator(action.locator),
381+
},
382+
options: action.options,
383+
}
323384
case 'page.waitForNavigation':
324385
case 'page.close':
325386
case 'page.*':
@@ -330,7 +391,6 @@ function buildBrowserNodeGraphFromActions(browserActions: AnyBrowserAction[]) {
330391
case 'locator.check':
331392
case 'locator.uncheck':
332393
case 'locator.selectOption':
333-
case 'locator.waitFor':
334394
case 'locator.hover':
335395
case 'locator.setChecked':
336396
case 'locator.tap':
@@ -366,7 +426,7 @@ export function convertEventsToTest({ browserEvents }: Recording): Test {
366426
export function convertActionsToTest({
367427
browserActions,
368428
}: {
369-
browserActions: AnyBrowserAction[]
429+
browserActions: BrowserActionInstance[]
370430
}): Test {
371431
return {
372432
defaultScenario: {

0 commit comments

Comments
 (0)