Skip to content

Commit edeb255

Browse files
strengthen up selector parsing - refs webdriverio/recorder-extension#8
1 parent 2388e2e commit edeb255

File tree

10 files changed

+1019
-868
lines changed

10 files changed

+1019
-868
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules
22
dist
33
.DS_Store
44
.vscode
5+
/*.js

Readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ WebdriverIO Chrome Recorder [![Build](https://github.com/webdriverio/chrome-reco
22
[![npm][npm-badge]][npm]
33
===========================
44

5-
This repo provide tools to convert JSON user flows from [Google Chrome DevTools Recorder](https://goo.gle/devtools-recorder) to WebdriverIO test scripts programmatically.
5+
This repo provide tools to convert JSON user flows from [Google Chrome DevTools Recorder](https://goo.gle/devtools-recorder) to WebdriverIO test scripts programmatically (WebdriverIO `v7.24.0` or higher required).
66

77
✅ Converts multiple recordings to WebdriverIO tests in one go (out-of-the-box glob support)
88
🗂 User can pass their custom path to export tests.

package-lock.json

Lines changed: 957 additions & 797 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,27 @@
3838
"devDependencies": {
3939
"@types/chai": "^4.3.3",
4040
"@types/inquirer": "^9.0.1",
41-
"@types/node": "^18.7.6",
41+
"@types/node": "^18.7.15",
4242
"@types/prettier": "^2.7.0",
43-
"@typescript-eslint/eslint-plugin": "^5.33.1",
44-
"@typescript-eslint/parser": "^5.33.1",
45-
"@vitest/coverage-c8": "^0.22.1",
43+
"@typescript-eslint/eslint-plugin": "^5.36.2",
44+
"@typescript-eslint/parser": "^5.36.2",
45+
"@vitest/coverage-c8": "^0.23.1",
4646
"chai": "^4.3.6",
47-
"eslint": "^8.22.0",
47+
"eslint": "^8.23.0",
4848
"eslint-config-prettier": "^8.5.0",
4949
"eslint-plugin-import": "^2.26.0",
5050
"eslint-plugin-prettier": "^4.2.1",
5151
"npm-run-all": "^4.1.5",
52-
"release-it": "^15.3.0",
52+
"release-it": "^15.4.1",
5353
"ts-node": "^10.9.1",
54-
"typescript": "^4.7.4",
55-
"vitest": "^0.22.1"
54+
"typescript": "^4.8.2",
55+
"vitest": "^0.23.1"
5656
},
5757
"dependencies": {
58-
"@puppeteer/replay": "^0.6.1",
58+
"@puppeteer/replay": "^1.1.2",
5959
"chalk": "^5.0.1",
6060
"globby": "^13.1.2",
61-
"inquirer": "^9.1.0",
61+
"inquirer": "^9.1.1",
6262
"meow": "^10.1.3",
6363
"prettier": "^2.7.1"
6464
},

src/cli/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import url from 'node:url'
2+
import path from 'node:path'
13
import meow from 'meow'
24
import inquirer from 'inquirer'
35

46
import { expandedFiles } from '../utils.js'
5-
import { DEFAULT_OUTPUT_FOLDER } from '../constants.js'
67
import { runTransformsOnChromeRecording } from '../transform.js'
78
import type { InquirerAnswerTypes } from '../types'
89

10+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
11+
912
const cli = meow(`
1013
1114
Usage
@@ -40,15 +43,15 @@ inquirer.prompt([
4043
message:
4144
'Which directory or files should be translated from Recorder JSON to WebdriverIO?',
4245
when: () => !cli.input.length,
43-
default: '.',
46+
default: path.join(__dirname, '*.json'),
4447
filter: (files: string) => files.split(/\s+/).filter((f) => f.trim().length > 0)
4548
},
4649
{
4750
type: 'input',
4851
name: 'outputPath',
4952
message: 'Where should be exported files to be output?',
5053
when: () => !cli.input.length,
51-
default: DEFAULT_OUTPUT_FOLDER,
54+
default: process.cwd(),
5255
}
5356
]).then((answers: InquirerAnswerTypes) => {
5457
const { files: recordingFiles, outputPath: outputFolder } = answers
@@ -66,7 +69,7 @@ inquirer.prompt([
6669

6770
return runTransformsOnChromeRecording({
6871
files: filesExpanded,
69-
outputPath: outputPath ?? DEFAULT_OUTPUT_FOLDER,
72+
outputPath: outputPath ?? process.cwd(),
7073
flags: cli.flags,
7174
})
7275
}).catch((error) => {

src/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,4 @@ export const SUPPORTED_KEYS = {
6464
meta: '\uE03D'
6565
} as const
6666

67-
export const DEFAULT_OUTPUT_FOLDER = 'wdio'
6867
export const KEY_NOT_SUPPORTED_ERROR = `key "%s" not supported (supported are ${Object.keys(SUPPORTED_KEYS).join(', ')}), please file an issue in https://github.com/webdriverio/chrome-recorder to let us know, so we can add support.`

src/stringifyExtension.ts

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
WaitForElementStep,
1818
WaitForExpressionStep
1919
} from '@puppeteer/replay'
20-
import { formatAsJSLiteral } from './utils.js'
20+
import { formatAsJSLiteral, findByCondition } from './utils.js'
2121
import { SUPPORTED_KEYS, KEY_NOT_SUPPORTED_ERROR } from './constants.js'
2222

2323
const ARIA_PREFIX = 'aria/'
@@ -259,51 +259,22 @@ export class StringifyExtension extends PuppeteerStringifyExtension {
259259

260260
getSelector(selectors: Selector[], flow: UserFlow): string | undefined {
261261
/**
262-
* check if selector is a link, e.g.
263-
* ```
264-
* "selectors": [
265-
* [
266-
* "aria/Timeouts"
267-
* ],
268-
* [
269-
* "#__docusaurus > div.main-wrapper.docs-wrapper.docs-doc-page > div > aside > div > nav > ul > li:nth-child(4) > ul > li:nth-child(2) > a"
270-
* ]
271-
* ],
272-
* ```
273-
* then use link selector
262+
* find by id first as it is the safest selector
274263
*/
275-
if (
276-
Array.isArray(selectors[0]) && Array.isArray(selectors[1]) &&
277-
selectors[0][0].startsWith(ARIA_PREFIX) &&
278-
selectors[1][0].endsWith('> a')
279-
) {
280-
return formatAsJSLiteral(`=${selectors[0][0].slice(ARIA_PREFIX.length)}`)
281-
}
282-
264+
const idSelector = findByCondition(
265+
selectors,
266+
(s) => s.startsWith('#') && !s.includes(' ') && !s.includes('.') && !s.includes('>') && !s.includes('[') && !s.includes('~') && !s.includes(':')
267+
)
268+
if (idSelector) return idSelector
283269
/**
284-
* check if selector is an element with text, e.g.
285-
* ```
286-
* "selectors": [
287-
* [
288-
* "aria/Flat White $18.00"
289-
* ],
290-
* [
291-
* "#app > div:nth-child(4) > ul > li:nth-child(5) > h4"
292-
* ]
293-
* ],
294-
* ```
295-
* then use element with text selector: h4=Flat White $18.00
270+
* use WebdriverIOs aria selector
271+
* https://webdriver.io/docs/selectors#accessibility-name-selector
296272
*/
297-
if (
298-
Array.isArray(selectors[0]) && Array.isArray(selectors[1]) &&
299-
selectors[0][0].startsWith(ARIA_PREFIX) &&
300-
selectors[1][0].includes(' > ')
301-
) {
302-
const tagName = selectors[1][0].split('>').pop()!.trim()
303-
// replace "button:nth-child(1)" with "button"
304-
.split(':')[0]
305-
return formatAsJSLiteral(`${tagName}=${selectors[0][0].slice(ARIA_PREFIX.length)}`)
306-
}
273+
const ariaSelector = findByCondition(
274+
selectors,
275+
(s) => s.startsWith(ARIA_PREFIX)
276+
)
277+
if (ariaSelector) return ariaSelector
307278

308279
// Remove Aria selectors
309280
const nonAriaSelectors = this.filterArrayByString(selectors, ARIA_PREFIX)

src/transform.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'node:path'
22
import fs from 'node:fs/promises'
3+
import { constants } from 'node:fs'
34
import { format } from 'prettier'
45
import chalk from 'chalk'
56

@@ -19,7 +20,7 @@ export function formatParsedRecordingContent(
1920
}
2021

2122
export async function runTransformsOnChromeRecording({ files, outputPath, flags }: TransformOpts) {
22-
const outputFolder = path.join(__dirname, outputPath)
23+
const outputFolder = path.resolve(__dirname, outputPath)
2324
const { dry } = flags
2425

2526
return files.map(async (file) => {
@@ -28,9 +29,7 @@ export async function runTransformsOnChromeRecording({ files, outputPath, flags
2829
)
2930

3031
const recordingContent = await fs.readFile(file, 'utf-8')
31-
const stringifiedFile = await stringifyChromeRecording(
32-
recordingContent,
33-
)
32+
const stringifiedFile = await stringifyChromeRecording(recordingContent)
3433

3534
if (!stringifiedFile) {
3635
return
@@ -57,9 +56,9 @@ export async function runTransformsOnChromeRecording({ files, outputPath, flags
5756
})
5857
}
5958

60-
async function exportFileToFolder({ stringifiedFile, testName, outputPath, outputFolder }: ExportToFile): Promise<any> {
59+
async function exportFileToFolder({ stringifiedFile, testName, outputPath, outputFolder }: ExportToFile): Promise<unknown> {
6160
const folderPath = path.join('.', outputPath)
62-
const folderExists = await fs.access(folderPath, fs.constants.F_OK).then(
61+
const folderExists = await fs.access(folderPath, constants.F_OK).then(
6362
() => true,
6463
() => false
6564
)
@@ -81,18 +80,18 @@ async function exportFileToFolder({ stringifiedFile, testName, outputPath, outpu
8180
}
8281

8382
return fs.writeFile(
84-
path.join(outputFolder, `/${testName}.js`),
83+
path.join(outputFolder, `${testName}.js`),
8584
stringifiedFile
8685
).then(() => {
8786
console.log(
8887
chalk.green(
89-
`\n ${testName}.json exported to ${outputPath}/${testName}.js\n `,
88+
`✅ ${testName}.json exported to ${outputPath}/${testName}.js\n `,
9089
),
9190
)
9291
}, () => {
9392
console.log(
9493
chalk.red(
95-
`\n 😭 Something went wrong exporting ${outputPath}/${testName}.js \n`,
94+
`😭 Something went wrong exporting ${outputPath}/${testName}.js \n`,
9695
),
9796
)
9897
})

src/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { globbySync } from 'globby'
2+
import type { Selector } from '@puppeteer/replay'
23

34
export function expandedFiles(files: string[]): string[] {
45
const containsGlob = files.some((file: string) => file.includes('*'))
@@ -8,3 +9,20 @@ export function expandedFiles(files: string[]): string[] {
89
export function formatAsJSLiteral(value: string) {
910
return JSON.stringify(value)
1011
}
12+
13+
export function findByCondition(
14+
selectors: Selector[],
15+
condition: (selector: string) => boolean
16+
) {
17+
const ariaSelector = selectors.find((selector) => Array.isArray(selector)
18+
? selector.find(condition)
19+
: condition(selector) ?? selector
20+
)
21+
if (ariaSelector) {
22+
return formatAsJSLiteral(Array.isArray(ariaSelector)
23+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
24+
? ariaSelector.find(condition)!
25+
: ariaSelector
26+
)
27+
}
28+
}

test/stringifyExtension.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe('StringifyExtension', () => {
102102
const flow = { title: 'change step', steps: [step] }
103103
const writer = new InMemoryLineWriter(' ')
104104
await ext.stringifyStep(writer, step, flow)
105-
expect(writer.toString()).toBe('await browser.$("=Guides").setValue("webdriverio")\n')
105+
expect(writer.toString()).toBe('await browser.$("aria/Guides").setValue("webdriverio")\n')
106106
})
107107

108108
it('should fetch by text', async () => {
@@ -120,7 +120,7 @@ describe('StringifyExtension', () => {
120120
const flow = { title: 'change step', steps: [step] }
121121
const writer = new InMemoryLineWriter(' ')
122122
await ext.stringifyStep(writer, step, flow)
123-
expect(writer.toString()).toBe('await browser.$("h4=Flat White $18.00").setValue("webdriverio")\n')
123+
expect(writer.toString()).toBe('await browser.$("aria/Flat White $18.00").setValue("webdriverio")\n')
124124
})
125125

126126
it('should fetch by text with pseudo selector', async () => {
@@ -138,7 +138,7 @@ describe('StringifyExtension', () => {
138138
const flow = { title: 'change step', steps: [step] }
139139
const writer = new InMemoryLineWriter(' ')
140140
await ext.stringifyStep(writer, step, flow)
141-
expect(writer.toString()).toBe('await browser.$("button=Yes").setValue("webdriverio")\n')
141+
expect(writer.toString()).toBe('await browser.$("aria/Yes").setValue("webdriverio")\n')
142142
})
143143

144144
it('should correctly exports keyDown step', async () => {

0 commit comments

Comments
 (0)