]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10039 Ease changing of password settings
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Thu, 29 Mar 2018 14:40:55 +0000 (16:40 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 5 Apr 2018 18:20:47 +0000 (20:20 +0200)
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/settings/SettingsPage.java
server/sonar-web/src/main/js/apps/settings/__tests__/DefinitionActions-test.tsx
server/sonar-web/src/main/js/apps/settings/__tests__/__snapshots__/DefinitionActions-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js
tests/build.gradle
tests/src/test/java/org/sonarqube/tests/settings/SettingsSuite.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/settings/ValidatorsTest.java [new file with mode: 0644]

index bd03b0ea77d4e01cdf3e43f227794a7c6be80d59..9bb53d9b50fc1d6b960fddc02507635256dff9be 100644 (file)
@@ -23,6 +23,9 @@ import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Selenide;
 import com.codeborne.selenide.SelenideElement;
 import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+
+import static com.codeborne.selenide.Selectors.byText;
 
 public class SettingsPage {
 
@@ -55,6 +58,12 @@ public class SettingsPage {
     return this;
   }
 
+  public SettingsPage assertSettingValueIsNotedAsDefault(String settingKey) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find(".spacer-top.note")
+      .shouldBe(Condition.text("(default)"));
+    return this;
+  }
+
   public SettingsPage assertBooleanSettingValue(String settingKey, boolean value) {
     SelenideElement toggle = Selenide.$("button[name=\"settings[" + settingKey + "]\"]");
     if (value) {
@@ -65,6 +74,62 @@ public class SettingsPage {
     return this;
   }
 
+  public SettingsPage assertSettingValueCanBeSaved(String settingKey) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find(byText("Save"))
+      .should(Condition.exist)
+      .shouldNotBe(Condition.attribute("disabled"));
+    return this;
+  }
+
+  public SettingsPage assertSettingValueCannotBeSaved(String settingKey) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find(byText("Save"))
+      .should(Condition.exist)
+      .shouldBe(Condition.attribute("disabled"));
+    return this;
+  }
+
+  public SettingsPage assertSettingValueCanBeReset(String settingKey) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find(byText("Reset"))
+      .should(Condition.exist);
+    return this;
+  }
+
+  public SettingsPage assertSettingValueCanBeCanceled(String settingKey) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find(byText("Cancel"))
+      .should(Condition.exist);
+    return this;
+  }
+
+  public SettingsPage assertInputCount(String settingKey, int count) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").findAll("input").shouldHaveSize(count);
+    return this;
+  }
+
+  public SettingsPage changeSettingValue(String settingKey, String value) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find("input").val(value);
+    return this;
+  }
+
+  public SettingsPage changeSettingValue(String settingKey, int index, String value) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").findAll("input").get(index).val(value);
+    return this;
+  }
+
+  public SettingsPage clickOnCancel(String settingKey) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find(byText("Cancel")).click();
+    return this;
+  }
+
+  public SettingsPage removeFirstValue(String settingKey) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find(".button.js-remove-value.button-icon").click();
+    return this;
+  }
+
+  public SettingsPage sendDeleteKeyToSettingField(String settingKey) {
+    Selenide.$("[data-key=\"" + settingKey + "\"]").find("input").sendKeys(Keys.BACK_SPACE);
+    return this;
+  }
+
   public SettingsPage setStringValue(String settingKey, String value) {
     SelenideElement setting = Selenide.$(".settings-definition[data-key=\"" + settingKey + "\"]");
     setting.find("input").val(value);
index d5ab2ec0ba7081a27bd49a33d6e1a576512c77c8..f13959d9abf52e89c2d6ff4a38e4c22916ec32b0 100644 (file)
@@ -69,16 +69,12 @@ it('displays reset button when empty and not default', () => {
   expect(wrapper).toMatchSnapshot();
 });
 
-it('displays default value label when current value differs', () => {
-  const wrapper = shallowRender('foobar', true, false);
-  expect(wrapper).toMatchSnapshot();
-});
-
 function shallowRender(changedValue: string, hasError: boolean, isDefault: boolean) {
   return shallow(
     <DefinitionActions
       changedValue={changedValue}
       hasError={hasError}
+      hasValueChanged={changedValue !== ''}
       isDefault={isDefault}
       onCancel={() => {}}
       onReset={() => {}}
index 12095239f94a90062ba76432012549c2dc84d664..1a8c1227c20f0a57c350bf73f1ab3fa0f6109c70 100644 (file)
@@ -2,16 +2,6 @@
 
 exports[`disables save button on error 1`] = `
 <React.Fragment>
-  <div
-    className="spacer-top note"
-    style={
-      Object {
-        "lineHeight": "24px",
-      }
-    }
-  >
-    settings._default
-  </div>
   <div
     className="settings-definition-changes nowrap"
   >
@@ -34,16 +24,6 @@ exports[`disables save button on error 1`] = `
 
 exports[`displays cancel button when value changed and has error 1`] = `
 <React.Fragment>
-  <div
-    className="spacer-top note"
-    style={
-      Object {
-        "lineHeight": "24px",
-      }
-    }
-  >
-    settings._default
-  </div>
   <div
     className="settings-definition-changes nowrap"
   >
@@ -66,16 +46,6 @@ exports[`displays cancel button when value changed and has error 1`] = `
 
 exports[`displays cancel button when value changed and no error 1`] = `
 <React.Fragment>
-  <div
-    className="spacer-top note"
-    style={
-      Object {
-        "lineHeight": "24px",
-      }
-    }
-  >
-    settings._default
-  </div>
   <div
     className="settings-definition-changes nowrap"
   >
@@ -112,39 +82,10 @@ exports[`displays default message when value is default 1`] = `
     className="settings-definition-changes nowrap"
   >
     <Button
-      className="spacer-right button-success"
-      disabled={false}
-      onClick={[Function]}
-    >
-      save
-    </Button>
-    <Button
-      className="spacer-right button-link"
-      onClick={[Function]}
-    >
-      cancel
-    </Button>
-  </div>
-</React.Fragment>
-`;
-
-exports[`displays default value label when current value differs 1`] = `
-<React.Fragment>
-  <div
-    className="settings-definition-changes nowrap"
-  >
-    <Button
-      className="spacer-right button-success"
-      disabled={true}
-      onClick={[Function]}
-    >
-      save
-    </Button>
-    <Button
-      className="spacer-right button-link"
+      className="spacer-right"
       onClick={[Function]}
     >
-      cancel
+      reset_verb
     </Button>
     <span
       className="note"
@@ -162,25 +103,12 @@ exports[`displays reset button when empty and not default 1`] = `
   <div
     className="settings-definition-changes nowrap"
   >
-    <Button
-      className="spacer-right button-success"
-      disabled={true}
-      onClick={[Function]}
-    >
-      save
-    </Button>
     <Button
       className="spacer-right"
       onClick={[Function]}
     >
       reset_verb
     </Button>
-    <Button
-      className="spacer-right button-link"
-      onClick={[Function]}
-    >
-      cancel
-    </Button>
     <span
       className="note"
     >
@@ -194,16 +122,6 @@ exports[`displays reset button when empty and not default 1`] = `
 
 exports[`displays save button when it can be saved 1`] = `
 <React.Fragment>
-  <div
-    className="spacer-top note"
-    style={
-      Object {
-        "lineHeight": "24px",
-      }
-    }
-  >
-    settings._default
-  </div>
   <div
     className="settings-definition-changes nowrap"
   >
index cde7870ee16680bd0b3cc7e9a21acb7c3020c543..b2ed1582adab9dc9a6e191e218b0af43ca6812d7 100644 (file)
@@ -29,8 +29,15 @@ export default class InputForPassword extends React.PureComponent {
     changing: false
   };
 
+  componentWillReceiveProps(nextProps /*: Props*/) {
+    if (!nextProps.hasValueChanged && this.props.hasValueChanged) {
+      this.setState({ changing: false, value: '' });
+    }
+  }
+
   handleInputChange(e) {
-    this.setState({ value: e.target.value });
+    this.props.onChange(e.target.value);
+    this.setState({ changing: true, value: e.target.value });
   }
 
   handleChangeClick(e) {
@@ -39,57 +46,34 @@ export default class InputForPassword extends React.PureComponent {
     this.setState({ changing: true });
   }
 
-  handleCancelChangeClick(e) {
-    e.preventDefault();
-    e.target.blur();
-    this.setState({ changing: false, value: '' });
-  }
-
-  handleFormSubmit(e) {
-    e.preventDefault();
-    this.props.onChange(this.state.value);
-    this.setState({ changing: false, value: '' });
-  }
-
   renderInput() {
     return (
-      <div>
-        <form onSubmit={e => this.handleFormSubmit(e)}>
-          <input className="hidden" type="password" />
-          <input
-            value={this.state.value}
-            name={this.props.name}
-            className="js-password-input settings-large-input text-top"
-            type="password"
-            autoFocus={true}
-            autoComplete={false}
-            onChange={e => this.handleInputChange(e)}
-          />
-
-          <button className="spacer-left button-success">{translate('save')}</button>
-
-          <a className="spacer-left" href="#" onClick={e => this.handleCancelChangeClick(e)}>
-            {translate('cancel')}
-          </a>
-        </form>
-      </div>
+      <form>
+        <input className="hidden" type="password" />
+        <input
+          autoComplete="off"
+          autoFocus={this.state.changing}
+          className="js-password-input settings-large-input text-top"
+          name={this.props.name}
+          onChange={e => this.handleInputChange(e)}
+          type="password"
+          value={this.state.value}
+        />
+      </form>
     );
   }
 
   render() {
-    if (this.state.changing) {
+    const hasValue = !!this.props.value;
+
+    if (this.state.changing || !hasValue) {
       return this.renderInput();
     }
 
-    const hasValue = !!this.props.value;
-
     return (
       <div>
-        {hasValue && <i className="big-spacer-right icon-lock icon-gray" />}
-
-        <button onClick={e => this.handleChangeClick(e)}>
-          {hasValue ? translate('change_verb') : translate('set')}
-        </button>
+        <i className="big-spacer-right icon-lock icon-gray" />
+        <button onClick={e => this.handleChangeClick(e)}>{translate('change_verb')}</button>
       </div>
     );
   }
index 0506676493bb1213a4ee6a46cfa03dce3dfd205c..b30d3db8cc99e388f354569301ce889c6ca84b6a 100644 (file)
@@ -43,21 +43,6 @@ it('should open form', () => {
   expect(input.find('form').length).toBe(1);
 });
 
-it('should close form', () => {
-  const onChange = jest.fn();
-  const input = shallow(
-    <InputForPassword name="foo" value="bar" isDefault={false} onChange={onChange} />
-  );
-  const button = input.find('button');
-  expect(button.length).toBe(1);
-
-  click(button);
-  expect(input.find('form').length).toBe(1);
-
-  click(input.find('form').find('a'));
-  expect(input.find('form').length).toBe(0);
-});
-
 it('should set value', () => {
   const onChange = jest.fn(() => Promise.resolve());
   const input = shallow(
index ae2b73cf2cc4fd6967901ec6e090ea204cca2a89..666130b7e6f23130dcbaae502ffacf1a8ca3fd80 100644 (file)
@@ -102,6 +102,7 @@ task integrationTest(type: Test) {
         includeTestsMatching 'org.sonarqube.tests.authorization.*Suite'
         includeTestsMatching 'org.sonarqube.tests.measure.*Suite'
         includeTestsMatching 'org.sonarqube.tests.qualityGate.*Suite'
+        includeTestsMatching 'org.sonarqube.tests.settings.*Suite'
         includeTestsMatching 'org.sonarqube.tests.source.*Suite'
         break
       case 'Category2':
diff --git a/tests/src/test/java/org/sonarqube/tests/settings/SettingsSuite.java b/tests/src/test/java/org/sonarqube/tests/settings/SettingsSuite.java
new file mode 100644 (file)
index 0000000..0b73df2
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.tests.settings;
+
+import com.sonar.orchestrator.Orchestrator;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import util.ItUtils;
+
+import static util.ItUtils.xooPlugin;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+  ValidatorsTest.class
+})
+public class SettingsSuite {
+
+  @ClassRule
+  public static final Orchestrator ORCHESTRATOR = ItUtils.newOrchestratorBuilder()
+
+    .addPlugin(xooPlugin())
+
+    .build();
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/settings/ValidatorsTest.java b/tests/src/test/java/org/sonarqube/tests/settings/ValidatorsTest.java
new file mode 100644 (file)
index 0000000..99bd319
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.tests.settings;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.qa.util.Tester;
+import org.sonarqube.qa.util.pageobjects.settings.SettingsPage;
+import org.sonarqube.tests.component.ComponentSuite;
+import org.sonarqube.ws.Users;
+
+import static util.ItUtils.projectDir;
+
+public class ValidatorsTest {
+
+  private Users.CreateWsResponse.User adminUser;
+
+  @ClassRule
+  public static Orchestrator orchestrator = ComponentSuite.ORCHESTRATOR;
+
+  @Rule
+  public Tester tester = new Tester(orchestrator).disableOrganizations();
+
+  @Before
+  public void initUsers() {
+    adminUser = tester.users().generateAdministrator(u -> u.setLogin("admin-user").setPassword("admin-user"));
+    tester.users().generate(u -> u.setLogin("random-user").setPassword("random-user"));
+  }
+
+  @Test
+  public void main_settings_simple_value() {
+    SettingsPage page = tester.openBrowser().logIn().submitCredentials(adminUser.getLogin())
+      .openSettings(null);
+    tester.wsClient().users().skipOnboardingTutorial();
+
+    String elementSelector = "sonar.dbcleaner.hoursBeforeKeepingOnlyOneSnapshotByDay";
+    page.assertSettingValueIsNotedAsDefault(elementSelector)
+      .changeSettingValue(elementSelector, "1")
+      .assertSettingValueCanBeSaved(elementSelector)
+      .assertSettingValueCanBeCanceled(elementSelector);
+
+    page.sendDeleteKeyToSettingField(elementSelector)
+      .assertSettingValueCannotBeSaved(elementSelector)
+      .assertSettingValueCanBeReset(elementSelector)
+      .assertSettingValueCanBeCanceled(elementSelector);
+  }
+
+  @Test
+  public void main_settings_complex_value() {
+    SettingsPage page = tester.openBrowser().logIn().submitCredentials(adminUser.getLogin())
+      .openSettings(null);
+    tester.wsClient().users().skipOnboardingTutorial();
+
+    String elementSelector = "sonar.preview.excludePlugins";
+    page.assertSettingValueIsNotedAsDefault(elementSelector)
+      .removeFirstValue(elementSelector)
+      .assertSettingValueCanBeSaved(elementSelector)
+      .assertSettingValueCanBeCanceled(elementSelector);
+
+    page.clickOnCancel(elementSelector).assertSettingValueIsNotedAsDefault(elementSelector);
+  }
+
+  @Test
+  public void project_settings_simple_value() {
+    analyzeSample();
+
+    SettingsPage page = tester.openBrowser().logIn().submitCredentials(adminUser.getLogin())
+      .openSettings("sample");
+    tester.wsClient().users().skipOnboardingTutorial();
+
+    String elementSelector = "sonar.dbcleaner.hoursBeforeKeepingOnlyOneSnapshotByDay";
+    page.assertSettingValueIsNotedAsDefault(elementSelector)
+      .changeSettingValue(elementSelector, "1")
+      .assertSettingValueCanBeSaved(elementSelector)
+      .assertSettingValueCanBeCanceled(elementSelector);
+
+    page.sendDeleteKeyToSettingField(elementSelector)
+      .assertSettingValueCannotBeSaved(elementSelector)
+      .assertSettingValueCanBeReset(elementSelector)
+      .assertSettingValueCanBeCanceled(elementSelector);
+  }
+
+  @Test
+  public void project_settings_complex_value() {
+    analyzeSample();
+
+    SettingsPage page = tester.openBrowser().logIn().submitCredentials(adminUser.getLogin())
+      .openSettings("sample");
+    tester.wsClient().users().skipOnboardingTutorial();
+
+    String elementSelector = "sonar.issue.ignore.allfile";
+    page.openCategory("Analysis Scope")
+      .changeSettingValue(elementSelector, "foo")
+      .changeSettingValue(elementSelector, 1, "bar")
+      .assertSettingValueCanBeSaved(elementSelector)
+      .assertSettingValueCanBeCanceled(elementSelector);
+
+    page.clickOnCancel(elementSelector).assertInputCount(elementSelector, 1);
+  }
+
+  private void analyzeSample() {
+    SonarScanner scan = SonarScanner.create(projectDir("shared/xoo-sample"))
+      .setProperty("sonar.cpd.exclusions", "**/*");
+    orchestrator.executeBuild(scan);
+  }
+
+}