Profile Creation Feature Tests
This commit is contained in:
parent
eddca2e974
commit
1652094e05
|
@ -0,0 +1,44 @@
|
|||
Feature: Basic Profile Management
|
||||
Scenario: Error on Creating a Profile without a Display Name
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the button with tooltip "Add new profile"
|
||||
Then I expect the text 'Display Name' to be present
|
||||
And I expect the text 'New Password' to be present
|
||||
And I expect the text 'Please enter a display name' to be absent
|
||||
Then I tap the "button" widget with label "Add new profile"
|
||||
And I expect the text 'Please enter a display name' to be present
|
||||
And I take a screenshot
|
||||
|
||||
Scenario: Create Unencrypted Profile
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the button with tooltip "Add new profile"
|
||||
Then I expect the text 'Display Name' to be present
|
||||
And I expect the text 'New Password' to be present
|
||||
And I take a screenshot
|
||||
Then I tap the "passwordCheckBox" widget
|
||||
And I expect the text 'New Password' to be absent
|
||||
And I take a screenshot
|
||||
Then I fill the "displayNameFormElement" field with "Alice (<h1>hello</h1>)"
|
||||
Then I tap the "button" widget with label "Add new profile"
|
||||
And I expect a "ProfileRow" widget with text "Alice (<h1>hello</h1>)"
|
||||
And I take a screenshot
|
||||
Then I tap the "ProfileRow" widget with label "Alice (<h1>hello</h1>)"
|
||||
And I expect the text 'Alice (<h1>hello</h1>) » Conversations' to be present
|
||||
And I take a screenshot
|
||||
|
||||
Scenario: Create Encrypted Profile
|
||||
Given I wait until the widget with type 'ProfileMgrView' is present
|
||||
And I tap the button with tooltip "Add new profile"
|
||||
Then I expect the text 'Display Name' to be present
|
||||
And I expect the text 'New Password' to be present
|
||||
And I take a screenshot
|
||||
Then I fill the "displayNameFormElement" field with "Alice (Encrypted)"
|
||||
Then I fill the "passwordFormElement" field with "password1"
|
||||
Then I fill the "confirmPasswordFormElement" field with "password1"
|
||||
And I take a screenshot
|
||||
Then I tap the "button" widget with label "Add new profile"
|
||||
And I expect a "ProfileRow" widget with text "Alice (Encrypted)"
|
||||
And I take a screenshot
|
||||
Then I tap the "ProfileRow" widget with label "Alice (Encrypted)"
|
||||
And I expect the text 'Alice (Encrypted) » Conversations' to be present
|
||||
And I take a screenshot
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,72 +0,0 @@
|
|||
//import 'package:flutter_gherkin/flutter_gherkin_integration_test.dart'; // notice new import name
|
||||
import 'package:flutter_gherkin/flutter_gherkin.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gherkin/gherkin.dart';
|
||||
|
||||
// The application under test.
|
||||
import 'package:cwtch/main.dart' as app;
|
||||
import 'package:glob/glob.dart';
|
||||
|
||||
import 'hooks/env.dart';
|
||||
import 'steps/chat.dart';
|
||||
import 'steps/files.dart';
|
||||
import 'steps/form_elements.dart';
|
||||
import 'steps/overrides.dart';
|
||||
import 'steps/text.dart';
|
||||
import 'steps/utils.dart';
|
||||
|
||||
part 'gherkin_suite_test.g.dart';
|
||||
const REPLACED_BY_SCRIPT = <String>['integration_test/features/**.feature'];
|
||||
|
||||
@GherkinTestSuite(executionOrder: ExecutionOrder.alphabetical, featurePaths: <String>['./integration_test/features/05_p2p_chat/01_add_remove_block_archive.feature','./integration_test/features/05_p2p_chat/02_proto_invites.feature','./integration_test/features/05_p2p_chat/03_send_receive.feature','./integration_test/features/05_p2p_chat/04_special_messages.feature','./integration_test/features/05_p2p_chat/05_overlays_invite.feature','./integration_test/features/05_p2p_chat/06_overlays_file.feature','./integration_test/features/05_p2p_chat/07_overlays_image.feature'])
|
||||
void main() {
|
||||
executeTestSuite(
|
||||
FlutterTestConfiguration.DEFAULT([])
|
||||
..reporters = [
|
||||
StdoutReporter(MessageLevel.error)
|
||||
..setWriteLineFn(print)
|
||||
..setWriteFn(print),
|
||||
ProgressReporter()
|
||||
..setWriteLineFn(print)
|
||||
..setWriteFn(print),
|
||||
TestRunSummaryReporter()
|
||||
..setWriteLineFn(print)
|
||||
..setWriteFn(print),
|
||||
JsonReporter(
|
||||
writeReport: (_, __) => Future<void>.value(),
|
||||
),
|
||||
]
|
||||
..customStepParameterDefinitions = [
|
||||
SwitchStateParameter(),
|
||||
]
|
||||
..stepDefinitions = [
|
||||
// chat elements
|
||||
ExpectReply(),
|
||||
// form elements
|
||||
CheckSwitchState(),
|
||||
CheckSwitchStateWithText(),
|
||||
DropdownChoose(),
|
||||
// utils
|
||||
TakeScreenshot(),
|
||||
// overrides
|
||||
TapWidgetWithType(),
|
||||
TapFirstWidget(),
|
||||
WaitUntilTypeExists(),
|
||||
ExpectTextToBePresent(),
|
||||
ExpectWidgetWithTextWithin(),
|
||||
WaitUntilTextExists(),
|
||||
SwipeOnType(),
|
||||
// text
|
||||
TorVersionPresent(),
|
||||
TooltipTap(),
|
||||
// files
|
||||
FolderExists(),
|
||||
FileExists(),
|
||||
]
|
||||
..hooks = [
|
||||
ResetCwtchEnvironment(),
|
||||
AttachScreenshotOnFailedStepHook(),
|
||||
],
|
||||
(World world) => app.main(),
|
||||
);
|
||||
}
|
|
@ -50,7 +50,9 @@ void main() {
|
|||
TakeScreenshot(),
|
||||
// overrides
|
||||
TapWidgetWithType(),
|
||||
TapFirstWidget(),
|
||||
TapWidgetWithLabel(),
|
||||
ExpectWidgetWithText(),
|
||||
TapButtonWithText(),
|
||||
WaitUntilTypeExists(),
|
||||
ExpectTextToBePresent(),
|
||||
ExpectWidgetWithTextWithin(),
|
||||
|
|
|
@ -1,265 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'gherkin_suite_test.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// GherkinSuiteTestGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class _CustomGherkinIntegrationTestRunner extends GherkinIntegrationTestRunner {
|
||||
_CustomGherkinIntegrationTestRunner(
|
||||
TestConfiguration configuration,
|
||||
Future<void> Function(World) appMainFunction,
|
||||
) : super(configuration, appMainFunction);
|
||||
|
||||
@override
|
||||
void onRun() {
|
||||
testFeature2();
|
||||
}
|
||||
|
||||
void testFeature2() {
|
||||
runFeature(
|
||||
'Sending and receiving chat messages:',
|
||||
<String>['@env:aliceandbob1'],
|
||||
() {
|
||||
runScenario(
|
||||
'Bob receives the message from Alice',
|
||||
<String>['@env:aliceandbob1'],
|
||||
(TestDependencies dependencies) async {
|
||||
await runStep(
|
||||
'Given I wait until the widget with type "ProfileRow" is present',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I wait for 4 seconds',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'Given I tap the button that contains the text "Alice"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the button that contains the text "Bob"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I wait until the text "Contact is offline, messages can\'t be delivered right now" is absent',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'When I fill the "txtCompose" field with "hello! this is a test!"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the "btnSend" button',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'Then I expect a "MessageBubble" widget with text "hello! this is a test!\u202F" to be present within 5 seconds',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the back button',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the back button',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'Given I tap the button that contains the text "Bob"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the button that contains the text "Alice"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'Then I expect a "MessageBubble" widget with text "hello! this is a test!\u202F" to be present within 5 seconds',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
},
|
||||
onBefore: () async => onBeforeRunFeature(
|
||||
'Sending and receiving chat messages',
|
||||
<String>['@env:aliceandbob1'],
|
||||
),
|
||||
onAfter: null,
|
||||
);
|
||||
|
||||
runScenario(
|
||||
'Bob replies to a message from Alice',
|
||||
<String>['@env:aliceandbob1'],
|
||||
(TestDependencies dependencies) async {
|
||||
await runStep(
|
||||
'Given I wait until the widget with type "ProfileRow" is present',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I wait for 4 seconds',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'Given I tap the button that contains the text "Alice"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the button that contains the text "Bob"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I wait until the text "Contact is offline, messages can\'t be delivered right now" is absent',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'When I fill the "txtCompose" field with "hello! this is a test!"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the "btnSend" button',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'Then I expect a "MessageBubble" widget with text "hello! this is a test!\u202F" to be present within 5 seconds',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the back button',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the back button',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'Given I tap the button that contains the text "Bob"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the button that contains the text "Alice"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the button with tooltip "Reply to this message"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I fill the "txtCompose" field with "yay the test worked"',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I tap the "btnSend" button',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'Then I expect to see the message "yay the test worked\u202F" replying to "hello! this is a test!" within 5 seconds',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
|
||||
await runStep(
|
||||
'And I take a screenshot',
|
||||
<String>[],
|
||||
null,
|
||||
dependencies,
|
||||
);
|
||||
},
|
||||
onBefore: null,
|
||||
onAfter: () async => onAfterRunFeature(
|
||||
'Sending and receiving chat messages',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void executeTestSuite(
|
||||
TestConfiguration configuration,
|
||||
Future<void> Function(World) appMainFunction,
|
||||
) {
|
||||
_CustomGherkinIntegrationTestRunner(configuration, appMainFunction).run();
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
var reporter = require('cucumber-html-reporter');
|
||||
|
||||
var options = {
|
||||
theme: 'bootstrap',
|
||||
jsonFile: 'gherkin/reports/integration_response_data.json',
|
||||
output: 'gherkin/reports/index.html',
|
||||
reportSuiteAsScenarios: true,
|
||||
scenarioTimestamp: true,
|
||||
launchReport: true,
|
||||
metadata: {
|
||||
"App Version":"0.3.2",
|
||||
"Test Environment": "STAGING",
|
||||
"Browser": "Chrome 54.0.2840.98",
|
||||
"Platform": "Windows 10",
|
||||
"Parallel": "Scenarios",
|
||||
"Executed": "Remote"
|
||||
}
|
||||
};
|
||||
|
||||
reporter.generate(options);
|
|
@ -27,9 +27,41 @@ StepDefinitionGeneric TapWidgetWithType() {
|
|||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric TapFirstWidget() {
|
||||
StepDefinitionGeneric TapWidgetWithLabel() {
|
||||
return given2<String, String, FlutterWorld>(
|
||||
RegExp(r'I tap the {string} widget with label {string}$'),
|
||||
(ofType, text, context) async {
|
||||
final finder = context.world.appDriver.findByDescendant(
|
||||
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
|
||||
context.world.appDriver.findBy(text, FindType.text),
|
||||
firstMatchOnly: true);
|
||||
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
|
||||
//print(wdg.debugDescribeChildren().first.)
|
||||
await context.world.appDriver.tap(finder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric ExpectWidgetWithText() {
|
||||
return given2<String, String, FlutterWorld>(
|
||||
RegExp(r'I expect a {string} widget with text {string}$'),
|
||||
(ofType, text, context) async {
|
||||
final finder = context.world.appDriver.findByDescendant(
|
||||
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
|
||||
context.world.appDriver.findBy(text, FindType.text),
|
||||
firstMatchOnly: true);
|
||||
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
|
||||
//print(wdg.debugDescribeChildren().first.)
|
||||
await context.world.appDriver.isPresent(finder);
|
||||
await context.world.appDriver.waitForAppToSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StepDefinitionGeneric TapButtonWithText() {
|
||||
return given1<String, FlutterWorld>(
|
||||
RegExp(r'I tap the first {string} (?:button|element|label|icon|field|text|widget)$'),
|
||||
RegExp(r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'),
|
||||
(input1, context) async {
|
||||
final finder = context.world.appDriver.findByDescendant(
|
||||
context.world.appDriver.findBy(Flwtch, FindType.type),
|
||||
|
@ -195,6 +227,10 @@ Type widgetTypeByName(String input1) {
|
|||
return ProfileRow;
|
||||
case "TorIcon":
|
||||
return TorIcon;
|
||||
case "button":
|
||||
return ElevatedButton;
|
||||
case "ProfileRow":
|
||||
return ProfileRow;
|
||||
default:
|
||||
throw("Unknown type $input1. add to integration_test/features/overrides.dart");
|
||||
}
|
||||
|
|
|
@ -104,6 +104,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
height: 20,
|
||||
),
|
||||
CwtchTextField(
|
||||
key: Key("displayNameFormElement"),
|
||||
controller: ctrlrNick,
|
||||
autofocus: false,
|
||||
hintText: AppLocalizations.of(context)!.yourDisplayName,
|
||||
|
@ -146,6 +147,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
visible: Provider.of<ProfileInfoState>(context).onion.isEmpty,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||||
Checkbox(
|
||||
key: Key("passwordCheckBox"),
|
||||
value: usePassword,
|
||||
fillColor: MaterialStateProperty.all(theme.current().defaultButtonColor),
|
||||
activeColor: theme.current().defaultButtonActiveColor,
|
||||
|
@ -179,6 +181,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
height: 20,
|
||||
),
|
||||
CwtchPasswordField(
|
||||
key: Key("currentPasswordFormElement"),
|
||||
controller: ctrlrOldPass,
|
||||
autoFillHints: [AutofillHints.newPassword],
|
||||
validator: (value) {
|
||||
|
@ -204,6 +207,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
height: 20,
|
||||
),
|
||||
CwtchPasswordField(
|
||||
key: Key("passwordFormElement"),
|
||||
controller: ctrlrPass,
|
||||
validator: (value) {
|
||||
// Password field can be empty when just updating the profile, not on creation
|
||||
|
@ -224,6 +228,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
height: 20,
|
||||
),
|
||||
CwtchPasswordField(
|
||||
key: Key("confirmPasswordFormElement"),
|
||||
controller: ctrlrPass2,
|
||||
validator: (value) {
|
||||
// Password field can be empty when just updating the profile, not on creation
|
||||
|
|
|
@ -9,20 +9,26 @@ const hints = [AutofillHints.password];
|
|||
// Provides a styled Password Input Field for use in Form Widgets.
|
||||
// Callers must provide a text controller, label helper text and a validator.
|
||||
class CwtchPasswordField extends StatefulWidget {
|
||||
CwtchPasswordField({required this.controller, required this.validator, this.action, this.autofocus = false, this.autoFillHints = hints});
|
||||
CwtchPasswordField({required this.controller, required this.validator, this.action, this.autofocus = false, this.autoFillHints = hints, this.key});
|
||||
final TextEditingController controller;
|
||||
final FormFieldValidator validator;
|
||||
final Function(String)? action;
|
||||
final bool autofocus;
|
||||
final Iterable<String> autoFillHints;
|
||||
final Key? key;
|
||||
|
||||
@override
|
||||
_CwtchTextFieldState createState() => _CwtchTextFieldState();
|
||||
_CwtchPasswordTextFieldState createState() => _CwtchPasswordTextFieldState();
|
||||
}
|
||||
|
||||
class _CwtchTextFieldState extends State<CwtchPasswordField> {
|
||||
class _CwtchPasswordTextFieldState extends State<CwtchPasswordField> {
|
||||
bool obscureText = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// todo: translations
|
||||
|
@ -42,7 +48,6 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
|
|||
autofillHints: widget.autoFillHints,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
onFieldSubmitted: widget.action,
|
||||
textInputAction: TextInputAction.unspecified,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
|
|
|
@ -8,7 +8,7 @@ doNothing(String x) {}
|
|||
// Provides a styled Text Field for use in Form Widgets.
|
||||
// Callers must provide a text controller, label helper text and a validator.
|
||||
class CwtchTextField extends StatefulWidget {
|
||||
CwtchTextField({required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false});
|
||||
CwtchTextField({required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false, this.key});
|
||||
final TextEditingController controller;
|
||||
final String hintText;
|
||||
final FormFieldValidator? validator;
|
||||
|
@ -16,6 +16,7 @@ class CwtchTextField extends StatefulWidget {
|
|||
final bool autofocus;
|
||||
final bool multiLine;
|
||||
final bool number;
|
||||
final Key? key;
|
||||
|
||||
@override
|
||||
_CwtchTextFieldState createState() => _CwtchTextFieldState();
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
pkill tor
|
||||
paths=$(find . -wholename "./integration_test/features/05*/*.feature" | sort | sed -z "s/\\n/','/g;s/,'$//;s/^/'/")
|
||||
paths=$(find . -wholename "./integration_test/features/*/$1*.feature" | sort | sed -z "s/\\n/','/g;s/,'$//;s/^/'/")
|
||||
sed "s|featurePaths: REPLACED_BY_SCRIPT|featurePaths: <String>[$paths]|" integration_test/gherkin_suite_test.editable.dart > integration_test/gherkin_suite_test.dart
|
||||
flutter pub run build_runner clean
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
LD_LIBRARY_PATH=/home/erinn/Android/Goprojects/libcwtch-go/ CWTCH_HOME=./integration_test/env/temp/ flutter drive --driver=test_driver/integration_test_driver.dart --target=integration_test/gherkin_suite_test.dart
|
||||
LD_LIBRARY_PATH=./linux/ CWTCH_HOME=./integration_test/env/temp/ flutter drive --driver=test_driver/integration_test_driver.dart --target=integration_test/gherkin_suite_test.dart
|
||||
node index2.js
|
||||
xdg-open integration_test/gherkin/reports/cucumber_report.html
|
||||
|
|
Loading…
Reference in New Issue