From ecaa7b76acf29853ea01d5065e39083c50a8ca62 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Mon, 17 Feb 2025 16:02:47 +0200 Subject: [PATCH] fix: better ios step logging --- .../ios/__fixtures__/broken.jsonl | 24 ++ .../__fixtures__/date-picker-set-column.json | 16 + .../ios/__fixtures__/multi-tap.json | 13 + .../ios/__fixtures__/swipe-action.json | 20 ++ .../ios/__fixtures__/tap-with-ancestor.json | 20 +- .../ios/__fixtures__/web-scroll-to-view.json | 11 + .../ios-description-maker.test.ts.snap | 323 ++++++++++++++---- .../description-maker/ios/detox-payload.ts | 230 +++++++++++-- .../ios/formatters/action-formatters.ts | 34 +- .../ios/formatters/expectation-formatters.ts | 22 +- .../ios/formatters/message-formatters.ts | 22 +- .../ios/formatters/predicate-formatters.ts | 182 +++++++--- .../ios/formatters/utils.test.ts | 12 +- .../description-maker/ios/formatters/utils.ts | 4 +- .../ios/ios-description-maker.test.ts | 36 +- 15 files changed, 795 insertions(+), 174 deletions(-) create mode 100644 src/steps/description-maker/ios/__fixtures__/broken.jsonl create mode 100644 src/steps/description-maker/ios/__fixtures__/date-picker-set-column.json create mode 100644 src/steps/description-maker/ios/__fixtures__/multi-tap.json create mode 100644 src/steps/description-maker/ios/__fixtures__/swipe-action.json create mode 100644 src/steps/description-maker/ios/__fixtures__/web-scroll-to-view.json diff --git a/src/steps/description-maker/ios/__fixtures__/broken.jsonl b/src/steps/description-maker/ios/__fixtures__/broken.jsonl new file mode 100644 index 0000000..c7e5fe7 --- /dev/null +++ b/src/steps/description-maker/ios/__fixtures__/broken.jsonl @@ -0,0 +1,24 @@ +5 +[] +{} +"{}" +"abc" +{"type":"anything"} +{"type":"anything","params":null} +{"type":"deliverPayload","params":null} +{"type":"invoke","params":null} +{"type":"invoke","params":{"type":"action"}}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":null}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[]}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":null}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"unknown"}}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"and"}}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"and","predicates":null}}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"and","predicates":[]}}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"id"}}} +{"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"id","value":null}}} +{"type":"invoke","params":{"type":"action","action":"longPress","params":[],"predicate":{"type":"id","value":"draggable"},"targetElement":null}} +{"type":"invoke","params":{"type":"expectation","predicate":null,"modifiers":["not"],"expectation":"toExist"}} +{"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Father883","isRegex":false},{"type":"ancestor","predicate":{}}]},"modifiers":null,"expectation":"toExist"}} +{"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Father883","isRegex":false},{"type":"ancestor","predicate":{}}]},"modifiers":["not"],"expectation":null}} +{"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Father883","isRegex":false},{"type":"ancestor","predicate":{}}]},"modifiers":["not"],"expectation":"toBeWeird"}} diff --git a/src/steps/description-maker/ios/__fixtures__/date-picker-set-column.json b/src/steps/description-maker/ios/__fixtures__/date-picker-set-column.json new file mode 100644 index 0000000..0f8a0d4 --- /dev/null +++ b/src/steps/description-maker/ios/__fixtures__/date-picker-set-column.json @@ -0,0 +1,16 @@ +{ + "type": "invoke", + "params": { + "type": "action", + "action": "setColumnToValue", + "params": [ + 1, + "6" + ], + "predicate": { + "type": "id", + "value": "datePicker", + "isRegex": false + } + } +} diff --git a/src/steps/description-maker/ios/__fixtures__/multi-tap.json b/src/steps/description-maker/ios/__fixtures__/multi-tap.json new file mode 100644 index 0000000..ac355a5 --- /dev/null +++ b/src/steps/description-maker/ios/__fixtures__/multi-tap.json @@ -0,0 +1,13 @@ +{ + "type": "invoke", + "params": { + "type": "action", + "action": "multiTap", + "params": [3], + "predicate": { + "type": "id", + "value": "container", + "isRegex": false + } + } +} diff --git a/src/steps/description-maker/ios/__fixtures__/swipe-action.json b/src/steps/description-maker/ios/__fixtures__/swipe-action.json new file mode 100644 index 0000000..079bb38 --- /dev/null +++ b/src/steps/description-maker/ios/__fixtures__/swipe-action.json @@ -0,0 +1,20 @@ +{ + "type": "invoke", + "params": { + "type": "action", + "action": "swipe", + "atIndex": 0, + "params": [ + "down", + "fast", + 0.7, + null, + null + ], + "predicate": { + "type": "text", + "value": "Index", + "isRegex": false + } + } +} diff --git a/src/steps/description-maker/ios/__fixtures__/tap-with-ancestor.json b/src/steps/description-maker/ios/__fixtures__/tap-with-ancestor.json index 034eca0..5ca1074 100644 --- a/src/steps/description-maker/ios/__fixtures__/tap-with-ancestor.json +++ b/src/steps/description-maker/ios/__fixtures__/tap-with-ancestor.json @@ -4,12 +4,20 @@ "type": "action", "action": "tap", "predicate": { - "type": "id", - "value": "childButton", - "ancestor": { - "type": "id", - "value": "parentView" - } + "type": "and", + "predicates": [ + { + "type": "id", + "value": "childButton" + }, + { + "type": "ancestor", + "predicate": { + "type": "id", + "value": "parentView" + } + } + ] } } } diff --git a/src/steps/description-maker/ios/__fixtures__/web-scroll-to-view.json b/src/steps/description-maker/ios/__fixtures__/web-scroll-to-view.json new file mode 100644 index 0000000..0fb9dae --- /dev/null +++ b/src/steps/description-maker/ios/__fixtures__/web-scroll-to-view.json @@ -0,0 +1,11 @@ +{ + "type": "invoke", + "params": { + "type": "webAction", + "webPredicate": { + "type": "id", + "value": "bottomParagraph" + }, + "webAction": "scrollToView" + } +} diff --git a/src/steps/description-maker/ios/__snapshots__/ios-description-maker.test.ts.snap b/src/steps/description-maker/ios/__snapshots__/ios-description-maker.test.ts.snap index 57ce62b..9843efb 100644 --- a/src/steps/description-maker/ios/__snapshots__/ios-description-maker.test.ts.snap +++ b/src/steps/description-maker/ios/__snapshots__/ios-description-maker.test.ts.snap @@ -1,5 +1,82 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`iOS description maker should not fail on broken.jsonl fixture 1`] = ` +"> 5 + +> [] + +> {} + +> "{}" + +> "abc" + +> {"type":"anything"} + +> {"type":"anything","params":null} + +> {"type":"deliverPayload","params":null} +< Deliver payload ++ undefined + +> {"type":"invoke","params":null} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":null}} +< Activate a11y action on ? ++ {} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[]}} +< Activate a11y action on ? ++ {} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":null}} +< Activate a11y action on ? ++ {} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"unknown"}}} +< Activate a11y action on (?) ++ {} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"and"}}} +< Activate a11y action on (?) ++ {} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"and","predicates":null}}} +< Activate a11y action on ? ++ {} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"and","predicates":[]}}} +< Activate a11y action on (?) ++ {} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"id"}}} +< Activate a11y action on (?) ++ {} + +> {"type":"invoke","params":{"type":"action","action":"accessibilityAction","params":[],"predicate":{"type":"id","value":null}}} +< Activate a11y action on (?) ++ {} + +> {"type":"invoke","params":{"type":"action","action":"longPress","params":[],"predicate":{"type":"id","value":"draggable"},"targetElement":null}} +< Long press #draggable ++ {"id":"draggable"} + +> {"type":"invoke","params":{"type":"expectation","predicate":null,"modifiers":["not"],"expectation":"toExist"}} +< Expect ? not to exist ++ null + +> {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Father883","isRegex":false},{"type":"ancestor","predicate":{}}]},"modifiers":null,"expectation":"toExist"}} +< Expect (#Father883 inside (?)) to exist ++ {"id":"Father883"} + +> {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Father883","isRegex":false},{"type":"ancestor","predicate":{}}]},"modifiers":["not"],"expectation":null}} + +> {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Father883","isRegex":false},{"type":"ancestor","predicate":{}}]},"modifiers":["not"],"expectation":"toBeWeird"}} +< Expect (#Father883 inside (?)) not to be weird ++ {"id":"Father883"} +" +`; + exports[`iOS description maker should process everything.jsonl fixture 1`] = ` "> {"params":{"testerConnected":true,"appConnected":true},"type":"loginSuccess"} @@ -144,25 +221,27 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"duration":{"x":5,"y":5},"text":"Long Press on Top Left"} > {"type":"invoke","params":{"type":"action","action":"multiTap","params":[3],"predicate":{"type":"id","value":"UniqueId819","isRegex":false}}} +< Tap 3 times on #UniqueId819 ++ {"count":3,"id":"UniqueId819"} > {"type":"invoke","params":{"type":"action","action":"replaceText","params":["1600"],"predicate":{"type":"id","value":"UniqueId_AnimationsScreen_delay","isRegex":false}}} -< Replace text in #UniqueId_Animat…nsScreen_delay +< Replace text in #UniqueId_AnimationsScreen_delay + {"text":"1600","id":"UniqueId_AnimationsScreen_delay"} > {"type":"invoke","params":{"type":"action","action":"replaceText","params":["2000"],"predicate":{"type":"id","value":"UniqueId_AnimationsScreen_duration","isRegex":false}}} -< Replace text in #UniqueId_Animat…creen_duration +< Replace text in #UniqueId_AnimationsScreen_duration + {"text":"2000","id":"UniqueId_AnimationsScreen_duration"} > {"type":"invoke","params":{"type":"action","action":"replaceText","params":["4"],"predicate":{"type":"id","value":"UniqueId_AnimationsScreen_numberOfIterations","isRegex":false}}} -< Replace text in #UniqueId_Animat…erOfIterations +< Replace text in #UniqueId_Animations…_numberOfIterations + {"text":"4","id":"UniqueId_AnimationsScreen_numberOfIterations"} > {"type":"invoke","params":{"type":"action","action":"replaceText","params":["500"],"predicate":{"type":"id","value":"UniqueId_AnimationsScreen_delay","isRegex":false}}} -< Replace text in #UniqueId_Animat…nsScreen_delay +< Replace text in #UniqueId_AnimationsScreen_delay + {"text":"500","id":"UniqueId_AnimationsScreen_delay"} > {"type":"invoke","params":{"type":"action","action":"replaceText","params":["5000"],"predicate":{"type":"id","value":"UniqueId_AnimationsScreen_duration","isRegex":false}}} -< Replace text in #UniqueId_Animat…creen_duration +< Replace text in #UniqueId_AnimationsScreen_duration + {"text":"5000","id":"UniqueId_AnimationsScreen_duration"} > {"type":"invoke","params":{"type":"action","action":"scroll","params":[10,"down",null,null],"predicate":{"type":"id","value":"FSScrollActions.scrollView","isRegex":false},"while":{"type":"expectation","predicate":{"type":"text","value":"Text16","isRegex":false},"expectation":"toBeVisible","params":[100]}}} @@ -266,8 +345,12 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"edge":"top","id":"ScrollView161"} > {"type":"invoke","params":{"type":"action","action":"setColumnToValue","params":[0,"c"],"predicate":{"type":"id","value":"pickerView","isRegex":false}}} +< Set #pickerView column [0] to: c ++ {"id":"pickerView","column":0,"value":"c"} > {"type":"invoke","params":{"type":"action","action":"setColumnToValue","params":[1,"6"],"predicate":{"type":"id","value":"datePicker","isRegex":false}}} +< Set #datePicker column [1] to: 6 ++ {"id":"datePicker","column":1,"value":"6"} > {"type":"invoke","params":{"type":"action","action":"setDatePickerDate","params":["2019-02-06T05:10:00-08:00","ISO8601"],"predicate":{"type":"id","value":"datePicker","isRegex":false}}} < Set date picker #datePicker 2019-02-06T05:10:00-08:00 @@ -282,6 +365,8 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"id":"datePicker","date":"2023-01-11T10:41:26Z","format":"ISO8601"} > {"type":"invoke","params":{"type":"action","action":"swipe","atIndex":0,"params":["down","fast",0.7,null,null],"predicate":{"type":"text","value":"Index","isRegex":false}}} +< Swipe down on "Index" ++ {"direction":"down","speed":"fast","amount":0.7,"text":"Index"} > {"type":"invoke","params":{"type":"action","action":"takeScreenshot","params":["elementScreenshot.horiz"],"predicate":{"type":"id","value":"fancyElement","isRegex":false}}} @@ -386,11 +471,11 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"id":"UniqueId819"} > {"type":"invoke","params":{"type":"action","action":"tap","predicate":{"type":"id","value":"UniqueId_AnimationsScreen_enableLoop","isRegex":false}}} -< Tap #UniqueId_Animat…een_enableLoop +< Tap #UniqueId_AnimationsScreen_enableLoop + {"id":"UniqueId_AnimationsScreen_enableLoop"} > {"type":"invoke","params":{"type":"action","action":"tap","predicate":{"type":"id","value":"UniqueId_AnimationsScreen_startButton","isRegex":false}}} -< Tap #UniqueId_Animat…en_startButton +< Tap #UniqueId_AnimationsScreen_startButton + {"id":"UniqueId_AnimationsScreen_startButton"} > {"type":"invoke","params":{"type":"action","action":"tap","predicate":{"type":"id","value":"closeButton","isRegex":false}}} @@ -742,67 +827,67 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"text":"Type Working 123 אֱבּג абв!!!","id":"UniqueId937"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Father883","isRegex":false},{"type":"ancestor","predicate":{"type":"id","value":"Grandson883","isRegex":false}}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#Father883 AND [ancestor] = ) not to exist -+ {"id":"Father883"} +< Expect (#Father883 inside #Grandson883) not to exist ++ {"id":"Father883","ancestor_id":"Grandson883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Father883","isRegex":false},{"type":"descendant","predicate":{"type":"id","value":"Grandson883","isRegex":false}}]},"expectation":"toExist"}} -< Expect (#Father883 AND [descendant] = ) to exist -+ {"id":"Father883"} +< Expect (#Father883 containing #Grandson883) to exist ++ {"id":"Father883","descendant_id":"Grandson883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Grandfather883","isRegex":false},{"type":"ancestor","predicate":{"type":"id","value":"Grandson883","isRegex":false}}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#Grandfather883 AND [ancestor] = ) not to exist -+ {"id":"Grandfather883"} +< Expect (#Grandfather883 inside #Grandson883) not to exist ++ {"id":"Grandfather883","ancestor_id":"Grandson883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Grandfather883","isRegex":false},{"type":"descendant","predicate":{"type":"id","value":"Grandson883","isRegex":false}}]},"expectation":"toExist"}} -< Expect (#Grandfather883 AND [descendant] = ) to exist -+ {"id":"Grandfather883"} +< Expect (#Grandfather883 con…ining #Grandson883) to exist ++ {"id":"Grandfather883","descendant_id":"Grandson883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Grandson883","isRegex":false},{"type":"ancestor","predicate":{"type":"id","value":"Father883","isRegex":false}}]},"expectation":"toExist"}} -< Expect (#Grandson883 AND [ancestor] = ) to exist -+ {"id":"Grandson883"} +< Expect (#Grandson883 inside #Father883) to exist ++ {"id":"Grandson883","ancestor_id":"Father883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Grandson883","isRegex":false},{"type":"ancestor","predicate":{"type":"id","value":"Grandfather883","isRegex":false}}]},"expectation":"toExist"}} -< Expect (#Grandson883 AND [ancestor] = ) to exist -+ {"id":"Grandson883"} +< Expect (#Grandson883 inside #Grandfather883) to exist ++ {"id":"Grandson883","ancestor_id":"Grandfather883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Grandson883","isRegex":false},{"type":"ancestor","predicate":{"type":"id","value":"Son883","isRegex":false}}]},"expectation":"toExist"}} -< Expect (#Grandson883 AND [ancestor] = ) to exist -+ {"id":"Grandson883"} +< Expect (#Grandson883 inside #Son883) to exist ++ {"id":"Grandson883","ancestor_id":"Son883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Grandson883","isRegex":false},{"type":"descendant","predicate":{"type":"id","value":"Father883","isRegex":false}}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#Grandson883 AND [descendant] = ) not to exist -+ {"id":"Grandson883"} +< Expect (#Grandson883 containing #Father883) not to exist ++ {"id":"Grandson883","descendant_id":"Father883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Grandson883","isRegex":false},{"type":"descendant","predicate":{"type":"id","value":"Grandfather883","isRegex":false}}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#Grandson883 AND [descendant] = ) not to exist -+ {"id":"Grandson883"} +< Expect (#Grandson883 contai…ng #Grandfather883) not to exist ++ {"id":"Grandson883","descendant_id":"Grandfather883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Grandson883","isRegex":false},{"type":"descendant","predicate":{"type":"id","value":"Son883","isRegex":false}}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#Grandson883 AND [descendant] = ) not to exist -+ {"id":"Grandson883"} +< Expect (#Grandson883 containing #Son883) not to exist ++ {"id":"Grandson883","descendant_id":"Son883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Son883","isRegex":false},{"type":"ancestor","predicate":{"type":"id","value":"Grandson883","isRegex":false}}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#Son883 AND [ancestor] = ) not to exist -+ {"id":"Son883"} +< Expect (#Son883 inside #Grandson883) not to exist ++ {"id":"Son883","ancestor_id":"Grandson883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"Son883","isRegex":false},{"type":"descendant","predicate":{"type":"id","value":"Grandson883","isRegex":false}}]},"expectation":"toExist"}} -< Expect (#Son883 AND [descendant] = ) to exist -+ {"id":"Son883"} +< Expect (#Son883 containing #Grandson883) to exist ++ {"id":"Son883","descendant_id":"Grandson883"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"UniqueId345","isRegex":false},{"type":"label","value":"RandomJunk","isRegex":false}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#UniqueId345 AND "RandomJunk") not to exist +< Expect (#UniqueId345 && "RandomJunk") not to exist + {"id":"UniqueId345","label":"RandomJunk"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"UniqueId345","isRegex":false},{"type":"text","value":"ID","isRegex":false}]},"expectation":"toExist"}} -< Expect (#UniqueId345 AND "ID") to exist +< Expect (#UniqueId345 && "ID") to exist + {"id":"UniqueId345","text":"ID"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"UniqueId345","isRegex":false},{"type":"text","value":"RandomJunk","isRegex":false}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#UniqueId345 AND "RandomJunk") not to exist +< Expect (#UniqueId345 && "RandomJunk") not to exist + {"id":"UniqueId345","text":"RandomJunk"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"and","predicates":[{"type":"id","value":"UniqueId345","isRegex":false},{"type":"traits","value":["button"]}]},"modifiers":["not"],"expectation":"toExist"}} -< Expect (#UniqueId345 AND [button]) not to exist +< Expect (#UniqueId345 && [button]) not to exist + {"id":"UniqueId345","traits":["button"]} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"id","value":"AlertScreen.Text","isRegex":false},"expectation":"toHaveText","params":["Cancel Pressed"]}} @@ -850,19 +935,19 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"id":"UniqueId819","expected":"Taps: 3"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"id","value":"UniqueId_AnimationsScreen_afterAnimationText","isRegex":false},"expectation":"toBeVisible"}} -< Expect #UniqueId_Animat…rAnimationText to be visible +< Expect #UniqueId_Animations…_afterAnimationText to be visible + {"id":"UniqueId_AnimationsScreen_afterAnimationText"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"id","value":"UniqueId_AnimationsScreen_afterAnimationText","isRegex":false},"expectation":"toExist"}} -< Expect #UniqueId_Animat…rAnimationText to exist +< Expect #UniqueId_Animations…_afterAnimationText to exist + {"id":"UniqueId_AnimationsScreen_afterAnimationText"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"id","value":"UniqueId_AnimationsScreen_afterAnimationText","isRegex":false},"modifiers":["not"],"expectation":"toExist"}} -< Expect #UniqueId_Animat…rAnimationText not to exist +< Expect #UniqueId_Animations…_afterAnimationText not to exist + {"id":"UniqueId_AnimationsScreen_afterAnimationText"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"id","value":"UniqueId_AnimationsScreen_enableLoop","isRegex":false},"expectation":"toHaveValue","params":["1"]}} -< Expect #UniqueId_Animat…een_enableLoop to have value 1 +< Expect #UniqueId_AnimationsScreen_enableLoop to have value 1 + {"id":"UniqueId_AnimationsScreen_enableLoop","expected":"1"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"id","value":"calendar","isRegex":false},"expectation":"toBeVisible"}} @@ -1250,31 +1335,31 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"text":"/^[a-z]* working!+$/i"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Accessibility Action activate Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Accessibility A…ate Working!!!" to be visible +< Expect "Accessibility Actio…ctivate Working!!!" to be visible + {"text":"Accessibility Action activate Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Accessibility Action custom Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Accessibility A…tom Working!!!" to be visible +< Expect "Accessibility Action custom Working!!!" to be visible + {"text":"Accessibility Action custom Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Accessibility Action decrement Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Accessibility A…ent Working!!!" to be visible +< Expect "Accessibility Actio…crement Working!!!" to be visible + {"text":"Accessibility Action decrement Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Accessibility Action escape Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Accessibility A…ape Working!!!" to be visible +< Expect "Accessibility Action escape Working!!!" to be visible + {"text":"Accessibility Action escape Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Accessibility Action increment Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Accessibility A…ent Working!!!" to be visible +< Expect "Accessibility Actio…crement Working!!!" to be visible + {"text":"Accessibility Action increment Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Accessibility Action longpress Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Accessibility A…ess Working!!!" to be visible +< Expect "Accessibility Actio…ngpress Working!!!" to be visible + {"text":"Accessibility Action longpress Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Accessibility Action magicTap Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Accessibility A…Tap Working!!!" to be visible +< Expect "Accessibility Actio…agicTap Working!!!" to be visible + {"text":"Accessibility Action magicTap Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Active","isRegex":false},"expectation":"toBeVisible"}} @@ -1402,19 +1487,19 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"text":"Label Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Long Network Request Working!!!","isRegex":false},"expectation":"toBeVisible","timeout":4000}} -< Expect "Long Network Re…est Working!!!" to be visible +< Expect "Long Network Request Working!!!" to be visible + {"text":"Long Network Request Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Long Network Request Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Long Network Re…est Working!!!" to be visible +< Expect "Long Network Request Working!!!" to be visible + {"text":"Long Network Request Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Long Network Request Working!!!","isRegex":false},"modifiers":["not"],"expectation":"toBeVisible"}} -< Expect "Long Network Re…est Working!!!" not to be visible +< Expect "Long Network Request Working!!!" not to be visible + {"text":"Long Network Request Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Long Press With Duration Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Long Press With…ion Working!!!" to be visible +< Expect "Long Press With Duration Working!!!" to be visible + {"text":"Long Press With Duration Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Long Press Working!!!","isRegex":false},"expectation":"toBeVisible"}} @@ -1422,11 +1507,11 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"text":"Long Press Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Long Press on Top Left Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Long Press on T…eft Working!!!" to be visible +< Expect "Long Press on Top Left Working!!!" to be visible + {"text":"Long Press on Top Left Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Long Press on Top Left Working!!!","isRegex":false},"modifiers":["not"],"expectation":"toBeVisible"}} -< Expect "Long Press on T…eft Working!!!" not to be visible +< Expect "Long Press on Top Left Working!!!" not to be visible + {"text":"Long Press on Top Left Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Long Timeout Ignored!!!","isRegex":false},"expectation":"toBeVisible"}} @@ -1462,11 +1547,11 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"text":"Say World"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Screen Long Custom Duration Pressed","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Screen Long Cus…ration Pressed" to be visible +< Expect "Screen Long Custom Duration Pressed" to be visible + {"text":"Screen Long Custom Duration Pressed"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Screen Long Custom Duration Pressed","isRegex":false},"modifiers":["not"],"expectation":"toBeVisible"}} -< Expect "Screen Long Cus…ration Pressed" not to be visible +< Expect "Screen Long Custom Duration Pressed" not to be visible + {"text":"Screen Long Custom Duration Pressed"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Screen Long Pressed","isRegex":false},"expectation":"toBeVisible"}} @@ -1482,7 +1567,7 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"text":"Shaken, not stirred"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Short Network Request Working!!!","isRegex":false},"expectation":"toBeVisible"}} -< Expect "Short Network R…est Working!!!" to be visible +< Expect "Short Network Request Working!!!" to be visible + {"text":"Short Network Request Working!!!"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"Short Timeout Ignored!!!","isRegex":false},"expectation":"toBeVisible"}} @@ -1566,11 +1651,11 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"text":"com.test.itemId"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"detoxtesturlscheme://such-string?arg1=first&arg2=second","isRegex":false},"expectation":"toBeVisible"}} -< Expect "detoxtesturlsch…st&arg2=second" to be visible +< Expect "detoxtesturlscheme:…=first&arg2=second" to be visible + {"text":"detoxtesturlscheme://such-string?arg1=first&arg2=second"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"detoxtesturlscheme://such-string?arg1=first&arg2=second","isRegex":false},"modifiers":["not"],"expectation":"toBeVisible"}} -< Expect "detoxtesturlsch…st&arg2=second" not to be visible +< Expect "detoxtesturlscheme:…=first&arg2=second" not to be visible + {"text":"detoxtesturlscheme://such-string?arg1=first&arg2=second"} > {"type":"invoke","params":{"type":"expectation","predicate":{"type":"text","value":"https://my.deeplink.dtx","isRegex":false},"expectation":"toBeVisible"}} @@ -1590,128 +1675,252 @@ exports[`iOS description maker should process everything.jsonl fixture 1`] = ` + {"type":"RCTImageView"} > {"type":"invoke","params":{"type":"webAction","predicate":{"type":"id","value":"webView","isRegex":false},"webPredicate":{"type":"tag","value":"body"},"webAction":"getTitle"}} +< Web action: getTitle ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"bottomParagraph"},"webAction":"scrollToView"}} +< Web action: scrollToView ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"clearText"}} +< Web action: clearText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"focus"}} +< Web action: focus ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"moveCursorToEnd"}} +< Web action: moveCursorToEnd ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"replaceText","params":["Tester"]}} +< Web action: replaceText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"selectAllText"}} +< Web action: selectAllText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"typeText","params":["Tes"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"typeText","params":["Test"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"typeText","params":["er"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"typeText","params":["r"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"contentEditable"},"webAction":"typeText","params":["te"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"clearText"}} +< Web action: clearText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"focus"}} +< Web action: focus ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"moveCursorToEnd"}} +< Web action: moveCursorToEnd ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"replaceText","params":["Tester"]}} +< Web action: replaceText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"runScript","params":["el => {\\n el.type = 'email';\\n }"]}} +< Web action: runScript ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"selectAllText"}} +< Web action: selectAllText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"typeText","params":["0123456789 ABCDEF"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"typeText","params":["Temp"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"typeText","params":["Test"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"typeText","params":["Tester"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"fname"},"webAction":"typeText","params":["er"]}} +< Web action: typeText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"pageHeadline"},"webAction":"getText"}} +< Web action: getText ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"pageHeadline"},"webAction":"runScript","params":["(el) => { el.textContent = \\"Changed\\"; throw new Error(\\"Error\\"); }"]}} +< Web action: runScript ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"pageHeadline"},"webAction":"runScript","params":["(el) => { el.textContent = \\"Changed\\"; }"]}} +< Web action: runScript ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"pageHeadline"},"webAction":"runScript","params":["(el) => { return el.textContent; }"]}} +< Web action: runScript ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"pageHeadline"},"webAction":"runScript","params":["el => {\\n el.textContent = \\"Changed\\";\\n }"]}} +< Web action: runScript ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"pageHeadline"},"webAction":"runScriptWithArgs","params":["(el, text) => { el.textContent = text; }",["Changed"]]}} +< Web action: runScriptWithArgs ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"pageHeadline"},"webAction":"runScriptWithArgs","params":["(el, text) => {\\n el.textContent = text;\\n }",["Changed"]]}} +< Web action: runScriptWithArgs ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"submit"},"webAction":"tap"}} +< Web action: tap ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"id","value":"w3link"},"webAction":"tap"}} +< Web action: tap ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"tag","value":"body"},"webAction":"getCurrentUrl"}} +< Web action: getCurrentUrl ++ undefined > {"type":"invoke","params":{"type":"webAction","webPredicate":{"type":"tag","value":"body"},"webAction":"getTitle"}} +< Web action: getTitle ++ undefined > {"type":"invoke","params":{"type":"webExpectation","predicate":{"type":"id","value":"webView","isRegex":false},"atIndex":0,"webPredicate":{"type":"id","value":"message"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","predicate":{"type":"id","value":"webView","isRegex":false},"atIndex":1,"webPredicate":{"type":"id","value":"message"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","predicate":{"type":"id","value":"webView","isRegex":false},"atIndex":2,"webPredicate":{"type":"id","value":"message"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","predicate":{"type":"id","value":"webView","isRegex":false},"webPredicate":{"type":"id","value":"message"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","predicate":{"type":"id","value":"webView","isRegex":false},"webPredicate":{"type":"id","value":"message"},"webExpectation":"toHaveText","params":["This is a dummy webview."]}} +< Web expectation: toHaveText ++ undefined > {"type":"invoke","params":{"type":"webExpectation","predicate":{"type":"id","value":"webView","isRegex":false},"webPredicate":{"type":"tag","value":"h1"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","predicate":{"type":"id","value":"webView","isRegex":false},"webPredicate":{"type":"tag","value":"h1"},"webModifiers":["not"],"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"class","value":"specialParagraph"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"css","value":".specialParagraph"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"href","value":"https://www.w3schools.com"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"hrefContains","value":"w3schools"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"contentEditable"},"webExpectation":"toHaveText","params":[""]}} +< Web expectation: toHaveText ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"contentEditable"},"webExpectation":"toHaveText","params":["Name: Tester"]}} +< Web expectation: toHaveText ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"contentEditable"},"webExpectation":"toHaveText","params":["Tester"]}} +< Web expectation: toHaveText ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"fname"},"webExpectation":"toHaveText","params":[""]}} +< Web expectation: toHaveText ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"fname"},"webExpectation":"toHaveText","params":["0123456789"]}} +< Web expectation: toHaveText ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"fname"},"webExpectation":"toHaveText","params":["Tester"]}} +< Web expectation: toHaveText ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"nonExistentElement"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"nonExistentElement"},"webModifiers":["not"],"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"pageHeadline"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"id","value":"pageHeadline"},"webExpectation":"toHaveText","params":["Changed"]}} +< Web expectation: toHaveText ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"label","value":"first-webview"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"name","value":"fname"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"tag","value":"body"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"tag","value":"p"},"webAtIndex":0,"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"tag","value":"p"},"webAtIndex":100,"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"tag","value":"p"},"webAtIndex":100,"webModifiers":["not"],"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"invoke","params":{"type":"webExpectation","webPredicate":{"type":"xpath","value":"//p[@class=\\"specialParagraph\\"]"},"webExpectation":"toExist"}} +< Web expectation: toExist ++ undefined > {"type":"isReady","params":{}} diff --git a/src/steps/description-maker/ios/detox-payload.ts b/src/steps/description-maker/ios/detox-payload.ts index 1d0e2bc..c8b2302 100644 --- a/src/steps/description-maker/ios/detox-payload.ts +++ b/src/steps/description-maker/ios/detox-payload.ts @@ -9,30 +9,69 @@ export type PredicateType = | 'accessibilityLabel' | 'accessibilityIdentifier'; -export interface Predicate { - type: PredicateType | 'and'; - value: string | string[] | number | boolean; +export type Predicate = + | AtomicPredicate + | AncestorPredicate + | DescendantPredicate + | CompoundPredicate; + +export interface CompoundPredicate { + type: 'and'; + predicates?: Predicate[]; +} + +export interface AncestorPredicate { + type: 'ancestor'; + predicate?: Predicate; +} + +export interface DescendantPredicate { + type: 'descendant'; + predicate?: Predicate; +} + +export interface AtomicPredicate { + type?: PredicateType; + value?: string | string[] | number | boolean; isRegex?: boolean; atIndex?: number; - predicates?: Predicate[]; - ancestor?: Predicate; - descendant?: Predicate; targetElement?: { predicate: Predicate; }; } //#endregion Predicates -//#region Detox Base Types +//#region Web Predicates +export type WebPredicateType = + | 'id' + | 'tag' + | 'class' + | 'css' + | 'href' + | 'hrefContains' + | 'name' + | 'label' + | 'xpath'; + +export interface WebPredicate { + type: WebPredicateType; + value: string; +} +//#endregion Web Predicates + +//#region Base Types export type DetoxAction = - | 'tap' + | 'accessibilityAction' | 'longPress' + | 'multiTap' + | 'replaceText' | 'scroll' | 'scrollTo' - | 'replaceText' - | 'typeText' - | 'accessibilityAction' - | 'setDatePickerDate'; + | 'setDatePickerDate' + | 'setColumnToValue' + | 'swipe' + | 'tap' + | 'typeText'; export type DetoxExpectation = | 'toBeVisible' @@ -47,30 +86,118 @@ export type ExpectationModifier = 'not'; export type ScrollDirection = 'up' | 'down' | 'left' | 'right'; export type ScrollEdge = 'top' | 'bottom' | 'left' | 'right'; -//#endregion Detox Base Types +//#endregion Base Types -//#region Base Invocation +//#region Web Types +export type WebAction = + | 'getTitle' + | 'getCurrentUrl' + | 'scrollToView' + | 'tap' + | 'clearText' + | 'focus' + | 'moveCursorToEnd' + | 'replaceText' + | 'selectAllText' + | 'typeText' + | 'getText' + | 'runScript' + | 'runScriptWithArgs'; + +export type WebExpectation = 'toExist' | 'toHaveText'; +//#endregion Web Types + +//#region Base Invocations interface BaseInvocation { - predicate: Predicate; + type?: string; + action?: unknown; + expectation?: unknown; + predicate?: Predicate; timeout?: number; atIndex?: number; params?: readonly unknown[]; } -//#endregion Base Invocation -//#region Invocations export interface ExpectationInvocation extends BaseInvocation { type: 'expectation'; - expectation: DetoxExpectation; + expectation?: DetoxExpectation; modifiers?: ExpectationModifier[]; } export interface BaseActionInvocation extends BaseInvocation { type: 'action'; - action: DetoxAction; + action?: DetoxAction; while?: ExpectationInvocation; } -//#endregion Invocations +//#endregion Base Invocations + +//#region Web Invocations +interface BaseWebInvocation extends BaseInvocation { + webPredicate?: WebPredicate; + webAtIndex?: number; +} + +interface BaseWebActionInvocation extends BaseWebInvocation { + type?: 'webAction'; + webAction?: WebAction; +} + +interface BaseWebExpectationInvocation extends BaseWebInvocation { + type?: 'webExpectation'; + webExpectation?: WebExpectation; + webModifiers?: ExpectationModifier[]; +} + +export interface WebGetTitleAction extends BaseWebActionInvocation { + type: 'webAction'; + webAction: 'getTitle'; +} + +export interface WebScrollToViewAction extends BaseWebActionInvocation { + type: 'webAction'; + webAction: 'scrollToView'; +} + +export interface WebTapAction extends BaseWebActionInvocation { + type: 'webAction'; + webAction: 'tap'; +} + +export interface WebTextAction extends BaseWebActionInvocation { + type: 'webAction'; + webAction?: + | 'clearText' + | 'focus' + | 'moveCursorToEnd' + | 'replaceText' + | 'selectAllText' + | 'typeText'; + params?: WebTextActionParams; +} + +export interface WebGetTextAction extends BaseWebActionInvocation { + type: 'webAction'; + webAction: 'getText'; +} + +export interface WebScriptAction extends BaseWebActionInvocation { + type: 'webAction'; + webAction: 'runScript'; + params?: WebScriptParams; +} + +export interface WebScriptWithArgsAction extends BaseWebActionInvocation { + type: 'webAction'; + webAction: 'runScriptWithArgs'; + params?: WebScriptWithArgsParams; +} + +export interface WebExpectationInvocation extends BaseWebExpectationInvocation { + type: 'webExpectation'; + webExpectation: WebExpectation; + params?: readonly [expected?: string]; +} +//#endregion Web Invocations //#region Action Invocations export interface TapAction extends BaseActionInvocation { @@ -111,6 +238,21 @@ export interface SetDatePickerAction extends BaseActionInvocation { action: 'setDatePickerDate'; params: DatePickerParams; } + +export interface SwipeAction extends BaseActionInvocation { + action: 'swipe'; + params: SwipeParams; +} + +export interface SetColumnAction extends BaseActionInvocation { + action: 'setColumnToValue'; + params: PickerColumnParams; +} + +export interface MultiTapAction extends BaseActionInvocation { + action: 'multiTap'; + params: MultiTapParams; +} //#endregion Action Invocations //#region Invocation Union Types @@ -122,9 +264,25 @@ export type ActionInvocation = | ReplaceTextAction | TypeTextAction | AccessibilityAction - | SetDatePickerAction; + | SetDatePickerAction + | SwipeAction + | SetColumnAction + | MultiTapAction; -export type Invocation = ActionInvocation | ExpectationInvocation; +export type WebInvocation = + | WebGetTitleAction + | WebScrollToViewAction + | WebTapAction + | WebTextAction + | WebGetTextAction + | WebScriptAction + | WebScriptWithArgsAction; + +export type Invocation = + | ActionInvocation + | ExpectationInvocation + | WebInvocation + | WebExpectationInvocation; //#endregion Invocation Union Types //#region Action Params @@ -138,29 +296,43 @@ export type LongPressParams = readonly [duration: number]; export type TextParams = readonly [text: string]; export type DatePickerParams = readonly [date: string, format?: string]; export type AccessibilityActionParams = readonly [action: string]; +export type SwipeParams = readonly [direction: string, speed: string, percentage: number]; +export type PickerColumnParams = readonly [column: number, value: string]; +export type MultiTapParams = readonly [count: number]; export type ActionParams = + | AccessibilityActionParams + | DatePickerParams + | LongPressParams + | MultiTapParams + | PickerColumnParams | ScrollParams | ScrollToParams - | LongPressParams - | TextParams - | DatePickerParams - | AccessibilityActionParams; + | SwipeParams + | TextParams; //#endregion Action Params +//#region Web Params +export type WebTextActionParams = readonly [text: string]; +export type WebScriptParams = readonly [script: string]; +export type WebScriptWithArgsParams = readonly [script: string, args: unknown[]]; + +export type WebActionParams = WebTextActionParams | WebScriptParams | WebScriptWithArgsParams; +//#endregion Web Params + //#region Messages export interface InvokeMessage { type: 'invoke'; - params: Invocation; + params?: Invocation; } export interface DeliverPayloadMessage { type: 'deliverPayload'; - params: DeliverPayloadParams; + params?: DeliverPayloadParams; } export interface DeliverPayloadParams { - url: string; + url?: string; delayPayload?: boolean; viewHierarchyURL?: string; detoxUserActivityDataURL?: string; diff --git a/src/steps/description-maker/ios/formatters/action-formatters.ts b/src/steps/description-maker/ios/formatters/action-formatters.ts index 07df420..b517f71 100644 --- a/src/steps/description-maker/ios/formatters/action-formatters.ts +++ b/src/steps/description-maker/ios/formatters/action-formatters.ts @@ -2,7 +2,7 @@ import type { StepDescription } from '../../types'; import type { ActionInvocation } from '../detox-payload'; import { formatWhileCondition } from './expectation-formatters'; import { formatPredicate as p } from './predicate-formatters'; -import { concat, msg } from './utils'; +import { concat, msg, truncate } from './utils'; type ActionFormatter = (action: T) => StepDescription; @@ -18,7 +18,7 @@ const actionFormatters: ActionFormatterMap = { scroll: ({ predicate, params: [distance, direction], while: whileCondition }) => { return concat( - msg(`Scroll ${direction} on`, { direction, distance }), + msg(`Scroll ${direction || 'somewhere'} on`, { direction, distance }), p(predicate), formatWhileCondition(whileCondition), ); @@ -26,7 +26,7 @@ const actionFormatters: ActionFormatterMap = { scrollTo: ({ predicate, params: [edge, normalizedX, normalizedY] }) => concat( - msg(`Scroll to ${edge}`, { + msg(`Scroll to ${edge || 'edge'}`, { edge, ...(normalizedX !== undefined && { x: normalizedX, y: normalizedY }), }), @@ -40,14 +40,38 @@ const actionFormatters: ActionFormatterMap = { typeText: ({ predicate, params: [text] }) => concat(msg('Type text in', { text }), p(predicate)), accessibilityAction: ({ predicate, params: [action] }) => - concat(msg(`Activate a11y "${action}" on`, { action }), p(predicate)), + concat(msg(`Activate a11y ${action ? `"${action}"` : 'action'} on`, { action }), p(predicate)), setDatePickerDate: ({ predicate, params: [date, format] }) => concat('Set date picker', p(predicate), msg(`${date}`, { date, format })), + + setColumnToValue: ({ predicate, params: [column, value] }) => + concat( + 'Set', + p(predicate), + 'column', + msg(`[${column}]`, { column }), + 'to:', + msg(`${truncate(value)}`, { value }), + ), + + multiTap: ({ predicate, params: [count] }) => + concat(msg(`Tap ${count} times on`, { count }), p(predicate)), + + swipe: ({ predicate, params: [direction, speed, amount] }) => + concat( + msg(`Swipe ${direction} on`, { + direction, + speed, + amount, + }), + p(predicate), + ), }; // Main entry point that routes to the correct formatter based on action type export const formatAction = (action: ActionInvocation): StepDescription | null => { const formatter = actionFormatters[action.action]; - return formatter ? formatter(action as any) : null; + const safeAction = action?.params ? action : { ...action, params: [] }; + return formatter ? formatter(safeAction as any) : null; }; diff --git a/src/steps/description-maker/ios/formatters/expectation-formatters.ts b/src/steps/description-maker/ios/formatters/expectation-formatters.ts index 3444a2d..8a4040a 100644 --- a/src/steps/description-maker/ios/formatters/expectation-formatters.ts +++ b/src/steps/description-maker/ios/formatters/expectation-formatters.ts @@ -1,19 +1,23 @@ import type { StepDescription } from '../../types'; -import type { ExpectationInvocation } from '../detox-payload'; +import type { ExpectationInvocation, Invocation } from '../detox-payload'; import { formatPredicate } from './predicate-formatters'; import { concat, msg, percent, truncate } from './utils'; const formatExpectationVerb = (invocation: ExpectationInvocation): string => { const hasNot = invocation.modifiers?.includes('not'); - const verb = invocation.expectation - .replace(/([A-Z])/g, ' $1') + const verb = invocation + .expectation!.replace(/([A-Z])/g, ' $1') .toLowerCase() .trim(); return `${hasNot ? 'not ' : ''}${verb}`; }; const formatExpectationParams = (invocation: ExpectationInvocation): StepDescription | null => { - const [expected] = invocation.params || []; + if (!Array.isArray(invocation.params)) { + return null; + } + + const [expected] = invocation.params; switch (invocation.expectation) { case 'toBeVisible': { @@ -38,11 +42,15 @@ export const formatWhileCondition = ( : null; }; -export const formatExpectation = (invocation: ExpectationInvocation): StepDescription => { +export const formatExpectation = (invocation: Invocation): StepDescription | null => { + if (!invocation.expectation) { + return null; + } + return concat( 'Expect', formatPredicate(invocation.predicate), - formatExpectationVerb(invocation), - formatExpectationParams(invocation), + formatExpectationVerb(invocation as ExpectationInvocation), + formatExpectationParams(invocation as ExpectationInvocation), ); }; diff --git a/src/steps/description-maker/ios/formatters/message-formatters.ts b/src/steps/description-maker/ios/formatters/message-formatters.ts index 4128616..0a0d1f6 100644 --- a/src/steps/description-maker/ios/formatters/message-formatters.ts +++ b/src/steps/description-maker/ios/formatters/message-formatters.ts @@ -15,18 +15,32 @@ const messageFormatters: MessageFormatterMap = { const invocation = message.params; return formatInvocation(invocation); }, - deliverPayload: ({ params: { url, delayPayload } }) => - msg('Deliver payload', { url, delayPayload }), + deliverPayload: (message) => { + const data = message.params + ? { + url: message.params.url, + delayPayload: message.params.delayPayload, + } + : undefined; + + return msg('Deliver payload', data); + }, }; -const formatInvocation = (invocation: Invocation): StepDescription | null => { - switch (invocation.type) { +const formatInvocation = (invocation?: Invocation): StepDescription | null => { + switch (invocation?.type) { case 'action': { return formatAction(invocation); } case 'expectation': { return formatExpectation(invocation); } + case 'webAction': { + return msg(`Web action: ${invocation.webAction}`); + } + case 'webExpectation': { + return msg(`Web expectation: ${invocation.webExpectation}`); + } default: { return null; } diff --git a/src/steps/description-maker/ios/formatters/predicate-formatters.ts b/src/steps/description-maker/ios/formatters/predicate-formatters.ts index a31b590..85aac88 100644 --- a/src/steps/description-maker/ios/formatters/predicate-formatters.ts +++ b/src/steps/description-maker/ios/formatters/predicate-formatters.ts @@ -1,14 +1,79 @@ import type { StepArgs, StepDescription } from '../../types'; -import type { Predicate } from '../detox-payload'; -import { concat, truncate } from './utils'; +import type { + AtomicPredicate, + Predicate, + CompoundPredicate, + DescendantPredicate, + AncestorPredicate, +} from '../detox-payload'; +import { concat, msg, truncate } from './utils'; -function join(a: string, b: string): string { - return a && b ? `${a}_${b}` : a || b; +export function formatPredicate(predicate?: Predicate, prefix = ''): StepDescription { + const result = _formatPredicate(predicate, prefix, false); + result.message = truncate(result.message); + + return result; } -function formatBasePredicate(predicate: Predicate, prefix = ''): StepDescription { - const { type, value, atIndex, isRegex } = predicate; - const _ = (name: string) => join(prefix, name); +function _formatPredicate( + predicate: Predicate | undefined, + prefix: string, + prependAND: boolean, +): StepDescription { + if (!predicate) { + return msg('?'); + } + + switch (predicate?.type) { + case 'and': { + return formatCompoundPredicate(predicate, prefix); + } + case 'descendant': { + return formatDescendantPredicate(predicate, prefix); + } + case 'ancestor': { + return formatAncestorPredicate(predicate, prefix); + } + default: { + const result = formatBasePredicate(predicate as AtomicPredicate, prefix); + return prependAND ? concat('&&', result) : result; + } + } +} + +function formatCompoundPredicate(predicate: CompoundPredicate, prefix = ''): StepDescription { + const { predicates = [] } = predicate; + if (!Array.isArray(predicates)) { + return msg('?'); + } + + const result = + predicates + .map((p: Predicate, index: number) => _formatPredicate(p, prefix, index > 0)) + .reduce((a: StepDescription | null, b: StepDescription) => (a ? concat(a, b) : b), null) ?? + msg('?'); + + result.message = `(${result.message})`; + return result; +} + +function formatDescendantPredicate(predicate: DescendantPredicate, prefix = ''): StepDescription { + const { predicate: descendant } = predicate; + return concat('containing', formatPredicate(descendant, join(prefix, 'descendant'))); +} + +function formatAncestorPredicate(predicate: AncestorPredicate, prefix = ''): StepDescription { + const { predicate: ancestor } = predicate; + return concat('inside', formatPredicate(ancestor, join(prefix, 'ancestor'))); +} + +function formatBasePredicate(predicate: AtomicPredicate, prefix = ''): StepDescription { + const { atIndex, isRegex, type, value } = predicate; + const _ = (name: string | undefined) => join(prefix, name); + + if (value == null || value === '') { + return msg('(?)'); + } const index$ = atIndex == null ? '' : `[${atIndex}]`; const args: StepArgs = { [_(type)]: value }; @@ -16,73 +81,78 @@ function formatBasePredicate(predicate: Predicate, prefix = ''): StepDescription args[_('index')] = atIndex; } - const value$ = truncate(value); + const value$ = String(value); - // Handle regex predicates if (isRegex) { - return { - message: `[${type}] ~ ${value$}` + index$, - args, - }; + return formatRegexPredicate(type, value$, index$, args); } - // Handle text predicates - if (type === 'label' || type === 'accessibilityLabel' || type === 'text') { - return { - message: `"${value$}"${index$ ? ' ' : ''}${index$}`, - args, - }; + if (isTextPredicate(type)) { + return formatTextPredicate(value$, index$, args); } - // Handle traits predicates if (type === 'traits') { - return { - message: `[${value$}]${index$}`, - args, - }; + return formatTraitsPredicate(value$, index$, args); } if (type === 'id') { - return { - message: `#${value$}${index$}`, - args, - }; + return formatIdPredicate(value$, index$, args); } + return formatDefaultPredicate(type, value$, index$, args); +} + +function formatRegexPredicate( + type: string | undefined, + value: string, + index: string, + args: StepArgs, +): StepDescription { return { - message: `[${type}] = ${value$}${index$}`, + message: `[${type}] ~ ${value}${index}`, args, }; } -export const formatPredicate = (predicate: Predicate, prefix = ''): StepDescription => { - const { predicates, ancestor, descendant } = predicate; +function isTextPredicate( + type: string | undefined, +): type is 'label' | 'accessibilityLabel' | 'text' { + return type === 'label' || type === 'accessibilityLabel' || type === 'text'; +} - // Handle compound predicates - if (predicates) { - const formattedPredicates = predicates.map((p) => formatPredicate(p)); - return { - message: `(${formattedPredicates.map((p) => p.message).join(' AND ')})`, - args: formattedPredicates.reduce((acc, p) => ({ ...acc, ...p.args }), {}), - }; - } +function formatTextPredicate(value: string, index: string, args: StepArgs): StepDescription { + return { + message: `"${value}"${index ? ' ' : ''}${index}`, + args, + }; +} - // Handle ancestor/descendant relationships - if (ancestor) { - return concat( - formatBasePredicate(predicate), - 'inside', - formatPredicate(ancestor, join(prefix, 'ancestor')), - ); - } +function formatTraitsPredicate(value: string, index: string, args: StepArgs): StepDescription { + return { + message: `[${value}]${index}`, + args, + }; +} - if (descendant) { - return concat( - formatBasePredicate(predicate), - 'containing', - formatPredicate(descendant, join(prefix, 'descendant')), - ); - } +function formatIdPredicate(value: string, index: string, args: StepArgs): StepDescription { + return { + message: `#${value}${index}`, + args, + }; +} + +function formatDefaultPredicate( + type: string | undefined, + value: string, + index: string, + args: StepArgs, +): StepDescription { + return { + message: `[${type}] = ${value}${index}`, + args, + }; +} - return formatBasePredicate(predicate, prefix); -}; +function join(a: string | undefined, b: string | undefined): string { + return a && b ? `${a}_${b}` : a || b || ''; +} diff --git a/src/steps/description-maker/ios/formatters/utils.test.ts b/src/steps/description-maker/ios/formatters/utils.test.ts index 3a65843..480f604 100644 --- a/src/steps/description-maker/ios/formatters/utils.test.ts +++ b/src/steps/description-maker/ios/formatters/utils.test.ts @@ -39,11 +39,11 @@ describe('truncate', () => { }); it('should truncate long string with ellipsis in the middle', () => { - const longString = '12345678901234567890123456789012345'; // 35 chars - const result = truncate(longString); + const longString = '123456'; + const result = truncate(longString, 5); - expect(result.length).toBe(30); // MAX_LENGTH - expect(result).toBe('123456789012345…23456789012345'); + expect(result.length).toBe(5); + expect(result).toBe('12…56'); }); it('should handle non-string values', () => { @@ -52,7 +52,7 @@ describe('truncate', () => { }); it('should handle string exactly at MAX_LENGTH', () => { - const exactString = '0'.repeat(30); + const exactString = '0'.repeat(40); expect(truncate(exactString)).toBe(exactString); }); @@ -60,7 +60,7 @@ describe('truncate', () => { // MAX_LENGTH (30) - 1 for ellipsis = 29 chars to distribute // Should be split as 15 chars front, 14 chars back const longString = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789'; - const result = truncate(longString); + const result = truncate(longString, 30); const frontPart = result.split('…')[0]; const backPart = result.split('…')[1]; diff --git a/src/steps/description-maker/ios/formatters/utils.ts b/src/steps/description-maker/ios/formatters/utils.ts index 002ee80..e971b99 100644 --- a/src/steps/description-maker/ios/formatters/utils.ts +++ b/src/steps/description-maker/ios/formatters/utils.ts @@ -7,7 +7,7 @@ export function concat(...results: (StepDescription | string | null)[]): StepDes }; } -export function msg(message: string, args: StepArgs): StepDescription { +export function msg(message: string, args?: StepArgs): StepDescription { return { message, args: omitEmpty(args), @@ -25,7 +25,7 @@ export function percent(value?: unknown): string { return Number.isFinite(num) ? (num * 100).toFixed(0) + '%' : ''; } -export function truncate(value?: unknown, maxLength = 30): string { +export function truncate(value?: unknown, maxLength = 40): string { if (!value) return ''; const str = typeof value === 'string' ? value : String(value); diff --git a/src/steps/description-maker/ios/ios-description-maker.test.ts b/src/steps/description-maker/ios/ios-description-maker.test.ts index 6da9c3f..2638567 100644 --- a/src/steps/description-maker/ios/ios-description-maker.test.ts +++ b/src/steps/description-maker/ios/ios-description-maker.test.ts @@ -52,11 +52,11 @@ describe('iOS description maker', () => { 'Scroll to top on #ScrollView161', { edge: 'top', id: 'ScrollView161' }, ], - ['tap-compound-and', 'Tap (#button AND "Click me")', { id: 'button', label: 'Click me' }], + ['tap-compound-and', 'Tap (#button && "Click me")', { id: 'button', label: 'Click me' }], ['tap-with-traits', 'Tap [button,selected]', { traits: ['button', 'selected'] }], [ 'tap-with-ancestor', - 'Tap #childButton inside #parentView', + 'Tap (#childButton inside #parentView)', { id: 'childButton', ancestor_id: 'parentView' }, ], [ @@ -64,6 +64,24 @@ describe('iOS description maker', () => { 'Scroll down on #ScrollView while waiting for "Text5" to be visible', { direction: 'down', distance: 50, id: 'ScrollView', while_text: 'Text5' }, ], + [ + 'swipe-action', + 'Swipe down on "Index"', + { text: 'Index', direction: 'down', speed: 'fast', amount: 0.7 }, + ], + [ + 'date-picker-set-column', + 'Set #datePicker column [1] to: 6', + { id: 'datePicker', column: 1, value: '6' }, + ], + [ + 'web-scroll-to-view', + // 'Scroll to #bottomParagraph in webview', + // { webId: 'bottomParagraph', webAction: 'scrollToView' }, + 'Web action: scrollToView', + undefined, + ], + ['multi-tap', 'Tap 3 times on #container', { id: 'container', count: 3 }], ])('should handle %s selector', (fixture, message, args) => { const description = iosDescriptionMaker(loadFixture(fixture)); expect(description).toEqual({ message, args }); @@ -82,4 +100,18 @@ describe('iOS description maker', () => { expect(results).toMatchSnapshot(); }); + + test('should not fail on broken.jsonl fixture', () => { + // Read and parse the JSONL file + const fileContent = fs.readFileSync( + path.join(__dirname, '__fixtures__', 'broken.jsonl'), + 'utf8', + ); + const lines = fileContent.split('\n').map(parseJsonLine).filter(Boolean); + + // Format each payload and join them together + const results = lines.map(formatPayloadAndDescription).join('\n'); + + expect(results).toMatchSnapshot(); + }); });