diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d2db7..8590b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [3.0.0-rc.13] - 27/06/2022 + - Fix #235 - fix issue taking a screenshot on an Android device + - Resolved #170: Added example code to ensure json report is save to disk even when the test run fails. Also added script to generate a HTML report from a JSON report + ## [3.0.0-rc.12] - 24/06/2022 - Fix #222 - escape single quotation marks in data tables diff --git a/example_with_integration_test/integration_test/features/create.feature b/example_with_integration_test/integration_test/features/create.feature index c5cd423..63c8a9f 100644 --- a/example_with_integration_test/integration_test/features/create.feature +++ b/example_with_integration_test/integration_test/features/create.feature @@ -7,6 +7,7 @@ Feature: Creating todos Then I expect the todo list | Todo | | Buy spinach | + When I take a screenshot called 'Johnson' Scenario: User can create multiple new todo items Given I fill the "todo" field with "Buy carrots" diff --git a/example_with_integration_test/integration_test/gherkin/configuration.dart b/example_with_integration_test/integration_test/gherkin/configuration.dart index cee1ff4..b8b4143 100644 --- a/example_with_integration_test/integration_test/gherkin/configuration.dart +++ b/example_with_integration_test/integration_test/gherkin/configuration.dart @@ -22,6 +22,7 @@ FlutterTestConfiguration gherkinTestConfiguration = FlutterTestConfiguration( ], hooks: [ ResetAppHook(), + // AttachScreenshotAfterStepHook(), ], reporters: [ StdoutReporter(MessageLevel.error) @@ -33,6 +34,9 @@ FlutterTestConfiguration gherkinTestConfiguration = FlutterTestConfiguration( TestRunSummaryReporter() ..setWriteLineFn(print) ..setWriteFn(print), + JsonReporter( + writeReport: (_, __) => Future.value(), + ), ], createWorld: (config) => Future.value(CustomWorld()), ); diff --git a/example_with_integration_test/integration_test/gherkin/hooks/attach_screenshot_after_step_hook.dart b/example_with_integration_test/integration_test/gherkin/hooks/attach_screenshot_after_step_hook.dart new file mode 100644 index 0000000..ceed4f8 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/hooks/attach_screenshot_after_step_hook.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; + +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; + +class AttachScreenshotAfterStepHook extends Hook { + @override + Future onAfterStep( + World world, + String step, + StepResult stepResult, + ) async { + try { + final screenshotData = await takeScreenshot(world); + world.attach(screenshotData, 'image/png', step); + } catch (e, st) { + world.attach('Failed to take screenshot\n$e\n$st', 'text/plain', step); + } + + return super.onAfterStep(world, step, stepResult); + } +} + +Future takeScreenshot(World world) async { + final bytes = await (world as FlutterWorld).appDriver.screenshot(); + + return base64Encode(bytes); +} diff --git a/example_with_integration_test/integration_test/gherkin/reports/cucumber_report.html b/example_with_integration_test/integration_test/gherkin/reports/cucumber_report.html deleted file mode 100644 index ccbd275..0000000 --- a/example_with_integration_test/integration_test/gherkin/reports/cucumber_report.html +++ /dev/null @@ -1,1173 +0,0 @@ - - - - Cucumber Feature Report - - - - - - - - -
- -
Wed Sep 15 2021 14:28:50 GMT+1000 (Australian Eastern Standard Time)
- -
-
-
-
- - - - - - -
- - -
- -
-
- -
-
- - - -
- -
-
-
- - - -

-

- - - - - - - Given - I fill the "todo" field with "Buy carrots" - - - - 637ms - - - - - - - - - - - - - - - - -
-

- - - -

-

- - - - - - - When - I tap the 'add' button - - - - 803ms - - - - - - - - - - - - - - - - -
-

- - - -

-

- - - - - - - Then - I expect the todo list - - - - 127ms - - - - - - -
- - - - - - - - - - - - - - - - - - -
Todo
- Buy carrots -
-
- -
- - - - - - - - - - -
-

- - -
-
-
- - -
- -
-
-
- - - -

-

- - - - - - - Given - I fill the "todo" field with "Buy carrots" - - - - 501ms - - - - - - - - - - - - - - - - -
-

- - - -

-

- - - - - - - When - I tap the "add" button - - - - 719ms - - - - - - - - - - - - - - - - -
-

- - - -

-

- - - - - - - And - I fill the "todo" field with "Buy apples" - - - - 379ms - - - - - - - - - - - - - - - - -
-

- - - -

-

- - - - - - - When - I tap the "add" button - - - - 722ms - - - - - - - - - - - - - - - - -
-

- - - -

-

- - - - - - - And - I fill the "todo" field with "Buy blueberries" - - - - 374ms - - - - - - - - - - - - - - - - -
-

- - - -

-

- - - - - - - When - I tap the "add" button - - - - 720ms - - - - - - - - - - - - - - - - -
-

- - - -

-

- - - - - - - Then - I expect the todo list - - - - 367ms - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Todo
- Buy blueberries -
- Buy apples -
- Buy carrots -
-
- -
- - - - - - - - - - -
-

- - - -

-

- - - - - - - Given - I wait 5 seconds for the animation to complete - - - - 113ms - - - - - - - - - - - - - - - - -
-

- - -
-
-
- -
-
-
-
- -
- - - - -
- -
-
- -
-
- - - -
- -
-
-
- - - -

-

- - - - - - - Given - I have item with data - - - - < 1ms - - - - - - - - - - - - - - - - -
-

- - -
-
-
- -
-
-
-
- -
- -
- - - -
- - - - - - - - - - - diff --git a/example_with_integration_test/integration_test/gherkin/reports/integration_response_data.json b/example_with_integration_test/integration_test/gherkin/reports/integration_response_data.json deleted file mode 100644 index eb77706..0000000 --- a/example_with_integration_test/integration_test/gherkin/reports/integration_response_data.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "gherkin_reports": "[[{\"description\":\"\",\"id\":\"checking data\",\"keyword\":\"Feature\",\"line\":1,\"name\":\"Checking data\",\"uri\":\"\",\"tags\":[{\"line\":1,\"name\":\"@tag\"}],\"elements\":[{\"keyword\":\"Scenario\",\"type\":\"scenario\",\"id\":\"checking data;user can have data\",\"name\":\"User can have data\",\"description\":\"\",\"line\":1,\"tags\":[{\"line\":1,\"name\":\"@tag\"},{\"line\":1,\"name\":\"@tag1\"}],\"steps\":[{\"keyword\":\"Given \",\"name\":\"I have item with data\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":4000000},\"docString\":{\"content_type\":\"\",\"value\":\"{\\n \\\"glossary\\\": {\\n \\\"title\\\": \\\"example glossary\\\",\\n \\\"GlossDiv\\\": {\\n \\\"title\\\": \\\"S\\\",\\n \\\"GlossList\\\": {\\n \\\"GlossEntry\\\": {\\n \\\"ID\\\": \\\"SGML\\\",\\n \\\"SortAs\\\": \\\"SGML\\\",\\n \\\"GlossTerm\\\": \\\"Standard Generalized Markup Language\\\",\\n \\\"Acronym\\\": \\\"SGML\\\",\\n \\\"Abbrev\\\": \\\"ISO 8879:1986\\\",\\n \\\"GlossDef\\\": {\\n \\\"para\\\": \\\"A meta-markup language, used to create markup languages such as DocBook.\\\",\\n \\\"GlossSeeAlso\\\": [\\n \\\"GML\\\",\\n \\\"XML\\\"\\n ]\\n },\\n \\\"GlossSee\\\": \\\"markup\\\"\\n }\\n }\\n }\\n }\\n}\",\"line\":2}}]}]},{\"description\":\"\",\"id\":\"swiping\",\"keyword\":\"Feature\",\"line\":1,\"name\":\"Swiping\",\"uri\":\"\",\"tags\":[{\"line\":1,\"name\":\"@tag\"}],\"elements\":[{\"keyword\":\"Scenario\",\"type\":\"scenario\",\"id\":\"swiping;user can swipe cards left and right\",\"name\":\"User can swipe cards left and right\",\"description\":\"\",\"line\":1,\"tags\":[{\"line\":1,\"name\":\"@tag\"},{\"line\":1,\"name\":\"@debug\"}],\"steps\":[{\"keyword\":\"Given \",\"name\":\"I swipe right by 250 pixels on the \\\"scrollable cards\\\"`\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":98000000}},{\"keyword\":\"Then \",\"name\":\"Then I expect the text \\\"Page 2\\\" to be present\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":7000000}},{\"keyword\":\"Given \",\"name\":\"I swipe left by 250 pixels on the \\\"scrollable cards\\\"`\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":10000000}},{\"keyword\":\"Then \",\"name\":\"Then I expect the text \\\"Page 1\\\" to be present\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":0}}]}]},{\"description\":\"\",\"id\":\"creating todos\",\"keyword\":\"Feature\",\"line\":1,\"name\":\"Creating todos\",\"uri\":\"\",\"tags\":[{\"line\":1,\"name\":\"@tag\"}],\"elements\":[{\"keyword\":\"Scenario\",\"type\":\"scenario\",\"id\":\"creating todos;user can create a new todo item\",\"name\":\"User can create a new todo item\",\"description\":\"\",\"line\":1,\"tags\":[{\"line\":1,\"name\":\"@tag\"},{\"line\":1,\"name\":\"@tag1\"},{\"line\":1,\"name\":\"@tag_two\"}],\"steps\":[{\"keyword\":\"Given \",\"name\":\"I fill the \\\"todo\\\" field with \\\"Buy carrots\\\"\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":594000000}},{\"keyword\":\"When \",\"name\":\"I tap the 'add' button\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":1110000000}},{\"keyword\":\"Then \",\"name\":\"I expect the todo list\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":113000000},\"rows\":[{\"cells\":[\"Todo\"]},{\"cells\":[\"Buy carrots\"]}]}]},{\"keyword\":\"Scenario\",\"type\":\"scenario\",\"id\":\"creating todos;user can create multiple new todo items\",\"name\":\"User can create multiple new todo items\",\"description\":\"\",\"line\":1,\"tags\":[{\"line\":1,\"name\":\"@tag\"},{\"line\":1,\"name\":\"@debug\"}],\"steps\":[{\"keyword\":\"Given \",\"name\":\"I fill the \\\"todo\\\" field with \\\"Buy carrots\\\"\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":465000000}},{\"keyword\":\"When \",\"name\":\"I tap the \\\"add\\\" button\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":723000000}},{\"keyword\":\"And \",\"name\":\"I fill the \\\"todo\\\" field with \\\"Buy apples\\\"\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":320000000}},{\"keyword\":\"When \",\"name\":\"I tap the \\\"add\\\" button\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":722000000}},{\"keyword\":\"And \",\"name\":\"I fill the \\\"todo\\\" field with \\\"Buy blueberries\\\"\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":320000000}},{\"keyword\":\"When \",\"name\":\"I tap the \\\"add\\\" button\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":724000000}},{\"keyword\":\"Then \",\"name\":\"I expect the todo list\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":347000000},\"rows\":[{\"cells\":[\"Todo\"]},{\"cells\":[\"Buy blueberries\"]},{\"cells\":[\"Buy apples\"]},{\"cells\":[\"Buy carrots\"]}]},{\"keyword\":\"Given \",\"name\":\"I wait 5 seconds for the animation to complete\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":104000000}},{\"keyword\":\"Given \",\"name\":\"I have item with data\",\"line\":1,\"match\":{\"location\":\":1\"},\"result\":{\"status\":\"passed\",\"duration\":0},\"docString\":{\"content_type\":\"\",\"value\":\"{\\n \\\"glossary\\\": {\\n \\\"title\\\": \\\"example glossary\\\",\\n \\\"GlossDiv\\\": {\\n \\\"title\\\": \\\"S\\\",\\n \\\"GlossList\\\": {\\n \\\"GlossEntry\\\": {\\n \\\"ID\\\": \\\"SGML\\\",\\n \\\"SortAs\\\": \\\"SGML\\\",\\n \\\"GlossTerm\\\": \\\"Standard Generalized Markup Language\\\",\\n \\\"Acronym\\\": \\\"SGML\\\",\\n \\\"Abbrev\\\": \\\"ISO 8879:1986\\\",\\n \\\"GlossDef\\\": {\\n \\\"para\\\": \\\"A meta-markup language, used to create markup languages such as DocBook.\\\",\\n \\\"GlossSeeAlso\\\": [\\n \\\"GML\\\",\\n \\\"XML\\\"\\n ]\\n },\\n \\\"GlossSee\\\": \\\"markup\\\"\\n }\\n }\\n }\\n }\\n}\",\"line\":2}}]}]}]]" -} \ No newline at end of file diff --git a/example_with_integration_test/integration_test/gherkin/reports/json_report.json b/example_with_integration_test/integration_test/gherkin/reports/json_report.json deleted file mode 100644 index a6a2754..0000000 --- a/example_with_integration_test/integration_test/gherkin/reports/json_report.json +++ /dev/null @@ -1 +0,0 @@ -[{"description":"","id":"user can create a new todo item","keyword":"Feature","line":1,"name":"User can create a new todo item","uri":"","tags":[{"line":1,"name":"@tag"},{"line":1,"name":"@tag1"},{"line":1,"name":"@tag_two"}],"elements":[{"keyword":"Scenario","type":"scenario","id":"user can create a new todo item;user can create a new todo item","name":"User can create a new todo item","description":"","line":1,"tags":[{"line":1,"name":"@tag"},{"line":1,"name":"@tag1"},{"line":1,"name":"@tag_two"}],"steps":[{"keyword":"Given ","name":"I fill the \"todo\" field with \"Buy carrots\"","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":637000000}},{"keyword":"When ","name":"I tap the 'add' button","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":803000000}},{"keyword":"Then ","name":"I expect the todo list","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":127000000},"rows":[{"cells":["Todo"]},{"cells":["Buy carrots"]}]}]},{"keyword":"Scenario","type":"scenario","id":"user can create a new todo item;user can create multiple new todo items","name":"User can create multiple new todo items","description":"","line":1,"tags":[{"line":1,"name":"@tag"},{"line":1,"name":"@debug"}],"steps":[{"keyword":"Given ","name":"I fill the \"todo\" field with \"Buy carrots\"","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":501000000}},{"keyword":"When ","name":"I tap the \"add\" button","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":719000000}},{"keyword":"And ","name":"I fill the \"todo\" field with \"Buy apples\"","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":379000000}},{"keyword":"When ","name":"I tap the \"add\" button","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":722000000}},{"keyword":"And ","name":"I fill the \"todo\" field with \"Buy blueberries\"","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":374000000}},{"keyword":"When ","name":"I tap the \"add\" button","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":720000000}},{"keyword":"Then ","name":"I expect the todo list","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":367000000},"rows":[{"cells":["Todo"]},{"cells":["Buy blueberries"]},{"cells":["Buy apples"]},{"cells":["Buy carrots"]}]},{"keyword":"Given ","name":"I wait 5 seconds for the animation to complete","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":113000000}}]}]},{"description":"","id":"user can have data","keyword":"Feature","line":1,"name":"User can have data","uri":"","tags":[{"line":1,"name":"@tag"},{"line":1,"name":"@tag1"}],"elements":[{"keyword":"Scenario","type":"scenario","id":"user can have data;user can have data","name":"User can have data","description":"","line":1,"tags":[{"line":1,"name":"@tag"},{"line":1,"name":"@tag1"}],"steps":[{"keyword":"Given ","name":"I have item with data","line":1,"match":{"location":":1"},"result":{"status":"passed","duration":0},"docString":{"content_type":"","value":"{\n \"glossary\": {\n \"title\": \"example glossary\",\n \"GlossDiv\": {\n \"title\": \"S\",\n \"GlossList\": {\n \"GlossEntry\": {\n \"ID\": \"SGML\",\n \"SortAs\": \"SGML\",\n \"GlossTerm\": \"Standard Generalized Markup Language\",\n \"Acronym\": \"SGML\",\n \"Abbrev\": \"ISO 8879:1986\",\n \"GlossDef\": {\n \"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\n \"GlossSeeAlso\": [\n \"GML\",\n \"XML\"\n ]\n },\n \"GlossSee\": \"markup\"\n }\n }\n }\n }\n}","line":2}}]}]}] \ No newline at end of file diff --git a/example_with_integration_test/integration_test/gherkin/reports/to_html_report.bat b/example_with_integration_test/integration_test/gherkin/reports/to_html_report.bat new file mode 100644 index 0000000..456e979 --- /dev/null +++ b/example_with_integration_test/integration_test/gherkin/reports/to_html_report.bat @@ -0,0 +1,5 @@ +call npm init -y + +call npm install --save-dev cucumber-html-reporter + +node -e "require('cucumber-html-reporter').generate({theme: 'bootstrap', jsonFile: 'REPORT_NAME.json', output: 'report.html', reportSuiteAsScenarios: true, launchReport: false});" \ No newline at end of file diff --git a/example_with_integration_test/integration_test/gherkin_suite_test.g.dart b/example_with_integration_test/integration_test/gherkin_suite_test.g.dart index 81f254d..70b5074 100644 --- a/example_with_integration_test/integration_test/gherkin_suite_test.g.dart +++ b/example_with_integration_test/integration_test/gherkin_suite_test.g.dart @@ -27,146 +27,6 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { } void testFeature0() { - runFeature( - name: 'Swiping:', - tags: ['@tag'], - run: () { - runScenario( - name: 'User can swipe cards left and right', - path: '.\\integration_test\\features\\swiping.feature', - tags: ['@tag', '@debug'], - steps: [ - ( - TestDependencies dependencies, - bool skip, - ) async { - return await runStep( - name: - 'Given I swipe right by 250 pixels on the "scrollable cards"`', - multiLineStrings: [], - table: null, - dependencies: dependencies, - skip: skip, - ); - }, - ( - TestDependencies dependencies, - bool skip, - ) async { - return await runStep( - name: 'Then I expect the text "Page 2" to be present', - multiLineStrings: [], - table: null, - dependencies: dependencies, - skip: skip, - ); - }, - ( - TestDependencies dependencies, - bool skip, - ) async { - return await runStep( - name: - 'Given I swipe left by 250 pixels on the "scrollable cards"`', - multiLineStrings: [], - table: null, - dependencies: dependencies, - skip: skip, - ); - }, - ( - TestDependencies dependencies, - bool skip, - ) async { - return await runStep( - name: 'Then I expect the text "Page 1" to be present', - multiLineStrings: [], - table: null, - dependencies: dependencies, - skip: skip, - ); - }, - ], - onBefore: () async => onBeforeRunFeature( - name: 'Swiping', - path: r'.\\integration_test\\features\\swiping.feature', - tags: ['@tag'], - ), - onAfter: () async => onAfterRunFeature( - name: 'Swiping', - path: r'.\\integration_test\\features\\swiping.feature', - tags: ['@tag'], - ), - ); - }, - ); - } - - void testFeature1() { - runFeature( - name: 'Checking data:', - tags: ['@tag'], - run: () { - runScenario( - name: 'User can have data', - path: '.\\integration_test\\features\\check.feature', - tags: ['@tag', '@tag1'], - steps: [ - ( - TestDependencies dependencies, - bool skip, - ) async { - return await runStep( - name: 'Given I have item with data', - multiLineStrings: [ - """{ - "glossary": { - "title": "example glossary", - "GlossDiv": { - "title": "S", - "GlossList": { - "GlossEntry": { - "ID": "SGML", - "SortAs": "SGML", - "GlossTerm": "Standard Generalized Markup Language", - "Acronym": "SGML", - "Abbrev": "ISO 8879:1986", - "GlossDef": { - "para": "A meta-markup language, used to create markup languages such as DocBook.", - "GlossSeeAlso": [ - "GML", - "XML" - ] - }, - "GlossSee": "markup" - } - } - } - } -}""" - ], - table: null, - dependencies: dependencies, - skip: skip, - ); - }, - ], - onBefore: () async => onBeforeRunFeature( - name: 'Checking data', - path: r'.\\integration_test\\features\\check.feature', - tags: ['@tag'], - ), - onAfter: () async => onAfterRunFeature( - name: 'Checking data', - path: r'.\\integration_test\\features\\check.feature', - tags: ['@tag'], - ), - ); - }, - ); - } - - void testFeature2() { runFeature( name: 'Creating todos:', tags: ['@tag'], @@ -212,6 +72,18 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { skip: skip, ); }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'When I take a screenshot called \'Johnson\'', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, ], onBefore: () async => onBeforeRunFeature( name: 'Creating todos', @@ -370,6 +242,146 @@ class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner { }, ); } + + void testFeature1() { + runFeature( + name: 'Swiping:', + tags: ['@tag'], + run: () { + runScenario( + name: 'User can swipe cards left and right', + path: '.\\integration_test\\features\\swiping.feature', + tags: ['@tag', '@debug'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: + 'Given I swipe right by 250 pixels on the "scrollable cards"`', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Then I expect the text "Page 2" to be present', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: + 'Given I swipe left by 250 pixels on the "scrollable cards"`', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Then I expect the text "Page 1" to be present', + multiLineStrings: [], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onBefore: () async => onBeforeRunFeature( + name: 'Swiping', + path: r'.\\integration_test\\features\\swiping.feature', + tags: ['@tag'], + ), + onAfter: () async => onAfterRunFeature( + name: 'Swiping', + path: r'.\\integration_test\\features\\swiping.feature', + tags: ['@tag'], + ), + ); + }, + ); + } + + void testFeature2() { + runFeature( + name: 'Checking data:', + tags: ['@tag'], + run: () { + runScenario( + name: 'User can have data', + path: '.\\integration_test\\features\\check.feature', + tags: ['@tag', '@tag1'], + steps: [ + ( + TestDependencies dependencies, + bool skip, + ) async { + return await runStep( + name: 'Given I have item with data', + multiLineStrings: [ + """{ + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": [ + "GML", + "XML" + ] + }, + "GlossSee": "markup" + } + } + } + } +}""" + ], + table: null, + dependencies: dependencies, + skip: skip, + ); + }, + ], + onBefore: () async => onBeforeRunFeature( + name: 'Checking data', + path: r'.\\integration_test\\features\\check.feature', + tags: ['@tag'], + ), + onAfter: () async => onAfterRunFeature( + name: 'Checking data', + path: r'.\\integration_test\\features\\check.feature', + tags: ['@tag'], + ), + ); + }, + ); + } } void executeTestSuite({ diff --git a/example_with_integration_test/pubspec.lock b/example_with_integration_test/pubspec.lock index 5bd4f82..3291425 100644 --- a/example_with_integration_test/pubspec.lock +++ b/example_with_integration_test/pubspec.lock @@ -213,7 +213,7 @@ packages: path: ".." relative: true source: path - version: "3.0.0" + version: "3.0.0-rc.13" flutter_simple_dependency_injection: dependency: "direct main" description: diff --git a/example_with_integration_test/test_driver/integration_test_driver.dart b/example_with_integration_test/test_driver/integration_test_driver.dart index b933b5e..a32b54c 100644 --- a/example_with_integration_test/test_driver/integration_test_driver.dart +++ b/example_with_integration_test/test_driver/integration_test_driver.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter_driver/flutter_driver.dart'; +import 'package:integration_test/common.dart'; import 'package:integration_test/integration_test_driver.dart' as integration_test_driver; @@ -22,7 +24,49 @@ Future main() { integration_test_driver.testOutputsDirectory = 'integration_test/gherkin/reports'; - return integration_test_driver.integrationDriver( - timeout: const Duration(minutes: 90), - ); + return integrationDriver(); +} + +// Rre-implement this rather than using `integration_test_driver.integrationDriver()` +// so that failed test runs will have reports saved to disk rather than just exiting +Future integrationDriver({ + Duration timeout = const Duration(minutes: 60), +}) async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String jsonResult = await driver.requestData(null, timeout: timeout); + final Response response = Response.fromJson(jsonResult); + + await driver.close(); + + final reports = json.decode(response.data!['gherkin_reports'].toString()) + as List; + + await writeGherkinReports(reports); + + if (response.allTestsPassed) { + exit(0); + } else { + print('Failure Details:\n${response.formattedFailureDetails}'); + exit(1); + } +} + +Future writeGherkinReports(List reports) async { + final filenamePrefix = + DateTime.now().toIso8601String().split('.').first.replaceAll(':', '-'); + + for (var i = 0; i < reports.length; i += 1) { + final reportData = reports.elementAt(i) as List; + + await fs + .directory(integration_test_driver.testOutputsDirectory) + .create(recursive: true); + File file = File( + '${integration_test_driver.testOutputsDirectory}/' + '$filenamePrefix' + 'v${i + 1}.json', + ); + + await file.writeAsString(json.encode(reportData)); + } } diff --git a/lib/flutter_gherkin.dart b/lib/flutter_gherkin.dart index 746c51d..ecd6cdd 100644 --- a/lib/flutter_gherkin.dart +++ b/lib/flutter_gherkin.dart @@ -30,6 +30,7 @@ export 'src/flutter/steps/wait_until_key_exists_step.dart'; export 'src/flutter/steps/when_tap_the_back_button_step.dart'; export 'src/flutter/steps/wait_until_type_exists_step.dart'; export 'src/flutter/steps/wait_until_key_exists_step.dart'; +export 'src/flutter/steps/take_a_screenshot_step.dart'; // Hooks export 'src/flutter/hooks/attach_screenshot_on_failed_step_hook.dart'; diff --git a/lib/src/flutter/adapters/app_driver_adapter.dart b/lib/src/flutter/adapters/app_driver_adapter.dart index 9cb508e..082eb66 100644 --- a/lib/src/flutter/adapters/app_driver_adapter.dart +++ b/lib/src/flutter/adapters/app_driver_adapter.dart @@ -57,7 +57,9 @@ abstract class AppDriverAdapter { ExpectedWidgetResultType expectResultType = ExpectedWidgetResultType.first, ]); - Future> screenshot(); + Future> screenshot({ + String? screenshotName, + }); Future isPresent( TFinderType finder, { diff --git a/lib/src/flutter/adapters/flutter_driver_app_driver_adapter.dart b/lib/src/flutter/adapters/flutter_driver_app_driver_adapter.dart index 77ef36c..fb423ed 100644 --- a/lib/src/flutter/adapters/flutter_driver_app_driver_adapter.dart +++ b/lib/src/flutter/adapters/flutter_driver_app_driver_adapter.dart @@ -43,7 +43,9 @@ class FlutterDriverAppDriverAdapter } @override - Future> screenshot() { + Future> screenshot({ + String? screenshotName, + }) { return nativeDriver.screenshot(); } diff --git a/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart b/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart index 98e9488..e7191fe 100644 --- a/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart +++ b/lib/src/flutter/adapters/widget_tester_app_driver_adapter.dart @@ -2,8 +2,10 @@ import 'dart:io' if (dart.library.html) 'dart:html'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'dart:ui' as ui show ImageByteFormat; import 'app_driver_adapter.dart'; @@ -66,15 +68,40 @@ class WidgetTesterAppDriverAdapter } } + Future> screenshotOnAndroid({String? screenshotName}) { + RenderObject? renderObject = binding.renderViewElement?.renderObject; + if (renderObject != null) { + while (!renderObject!.isRepaintBoundary) { + renderObject = renderObject.parent as RenderObject?; + assert(renderObject != null); + } + + final layer = renderObject.debugLayer as OffsetLayer; + + return layer + .toImage(renderObject.paintBounds) + .then((value) => value.toByteData(format: ui.ImageByteFormat.png)) + .then((value) => value!.buffer.asUint8List()); + } + + throw Exception('Unable to take screenshot on Android device'); + } + @override - Future> screenshot() async { + Future> screenshot({String? screenshotName}) async { if (!kIsWeb && Platform.isAndroid) { - await binding.convertFlutterSurfaceToImage(); - await binding.pump(); + return await screenshotOnAndroid(screenshotName: screenshotName); + // try { + // // TODO: See https://github.com/flutter/flutter/issues/92381 + // // we need to call `revertFlutterImage` once it has been implemented + // await binding.convertFlutterSurfaceToImage(); + // await binding.pump(); + // // ignore: no_leading_underscores_for_local_identifiers + // } catch (_, __) {} } return binding.takeScreenshot( - 'screenshot_${DateTime.now().millisecondsSinceEpoch}', + screenshotName ?? 'screenshot_${DateTime.now().millisecondsSinceEpoch}', ); } diff --git a/lib/src/flutter/configuration/flutter_test_configuration.dart b/lib/src/flutter/configuration/flutter_test_configuration.dart index 0fc4b80..d83526c 100644 --- a/lib/src/flutter/configuration/flutter_test_configuration.dart +++ b/lib/src/flutter/configuration/flutter_test_configuration.dart @@ -6,6 +6,8 @@ import 'package:flutter_gherkin/src/flutter/steps/when_long_press_widget_step.da import 'package:flutter_test/flutter_test.dart'; import 'package:gherkin/gherkin.dart'; +import '../steps/take_a_screenshot_step.dart'; + class FlutterTestConfiguration extends TestConfiguration { static final Iterable> _wellKnownParameters = [ ExistenceParameter(), @@ -35,6 +37,7 @@ class FlutterTestConfiguration extends TestConfiguration { textExistsWithinStep(), waitUntilKeyExistsStep(), waitUntilTypeExistsStep(), + takeScreenshot(), ]; /// Enable semantics in a test by creating a [SemanticsHandle]. diff --git a/lib/src/flutter/steps/take_a_screenshot_step.dart b/lib/src/flutter/steps/take_a_screenshot_step.dart new file mode 100644 index 0000000..a5467fd --- /dev/null +++ b/lib/src/flutter/steps/take_a_screenshot_step.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; + +import 'package:flutter_gherkin/flutter_gherkin.dart'; +import 'package:gherkin/gherkin.dart'; + +StepDefinitionGeneric takeScreenshot() { + return given1( + 'I take a screenshot called {String}', + (name, context) async { + final bytes = await context.world.appDriver.screenshot( + screenshotName: name, + ); + + context.world.attach(base64Encode(bytes), 'image/png'); + }, + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 9be8cf9..28f7003 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_gherkin description: A Gherkin / Cucumber parser and test runner for Dart and Flutter -version: 3.0.0-rc.12 +version: 3.0.0-rc.13 homepage: https://github.com/jonsamwell/flutter_gherkin environment: diff --git a/test/flutter_configuration_test.dart b/test/flutter_configuration_test.dart index 95d9d30..c51978b 100644 --- a/test/flutter_configuration_test.dart +++ b/test/flutter_configuration_test.dart @@ -18,7 +18,7 @@ void main() { test('common steps definition added', () { final config = FlutterDriverTestConfiguration(); expect(config.stepDefinitions, isNotNull); - expect(config.stepDefinitions!.length, 23); + expect(config.stepDefinitions!.length, 24); expect(config.customStepParameterDefinitions, isNotNull); expect(config.customStepParameterDefinitions!.length, 2); }); @@ -30,7 +30,7 @@ void main() { ); expect(config.stepDefinitions, isNotNull); - expect(config.stepDefinitions!.length, 24); + expect(config.stepDefinitions!.length, 25); expect( config.stepDefinitions!.elementAt(0), (x) => x is MockStepDefinition); expect(config.customStepParameterDefinitions, isNotNull);