From d7c2c71bd272dfc5006b77fa1587506df9cb021e Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Fri, 23 Nov 2018 16:41:46 +0100 Subject: [PATCH] SONARCLOUD-183 Advertise paid plan and recommend it to users --- server/sonar-web/src/main/js/app/types.d.ts | 2 + .../js/apps/create/components/CardPlan.css | 104 +++++++ .../js/apps/create/components/CardPlan.tsx | 167 ++++++++++ .../components/__tests__/CardPlan-test.tsx | 71 +++++ .../__snapshots__/CardPlan-test.tsx.snap | 294 ++++++++++++++++++ .../organization/AutoOrganizationCreate.tsx | 3 +- .../AutoPersonalOrganizationBind.tsx | 3 +- .../organization/ManualOrganizationCreate.tsx | 2 - .../apps/create/organization/PlanSelect.tsx | 66 ++-- .../js/apps/create/organization/PlanStep.tsx | 28 +- .../__tests__/AutoOrganizationCreate-test.tsx | 2 + .../AutoPersonalOrganizationBind-test.tsx | 2 + .../__tests__/CreateOrganization-test.tsx | 43 +-- .../ManualOrganizationCreate-test.tsx | 9 - .../__tests__/PlanSelect-test.tsx | 33 +- .../organization/__tests__/PlanStep-test.tsx | 10 +- .../RemoteOrganizationChoose-test.tsx | 9 +- .../AutoOrganizationCreate-test.tsx.snap | 46 ++- ...AutoPersonalOrganizationBind-test.tsx.snap | 22 +- .../CreateOrganization-test.tsx.snap | 6 + .../__snapshots__/PlanSelect-test.tsx.snap | 136 ++------ .../__snapshots__/PlanStep-test.tsx.snap | 27 +- .../icons-components/RecommendedIcon.tsx | 32 ++ .../helpers/__tests__/almIntegrations-test.ts | 8 +- .../resources/org/sonar/l10n/core.properties | 1 + 25 files changed, 915 insertions(+), 211 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/create/components/CardPlan.css create mode 100644 server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/icons-components/RecommendedIcon.tsx diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts index aad1cc73b48..c7ab0e8a71c 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -29,6 +29,8 @@ declare namespace T { export interface AlmOrganization extends OrganizationBase { key: string; personal: boolean; + privateRepos: number; + publicRepos: number; } export interface AlmRepository { diff --git a/server/sonar-web/src/main/js/apps/create/components/CardPlan.css b/server/sonar-web/src/main/js/apps/create/components/CardPlan.css new file mode 100644 index 00000000000..7b9f04e90c1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/CardPlan.css @@ -0,0 +1,104 @@ +/* + * 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. + */ + +.card-plan { + display: flex; + flex-direction: column; + width: 450px; + height: 210px; + background-color: #fff; + border: solid 1px var(--barBorderColor); + border-radius: 3px; + box-sizing: border-box; + margin-right: calc(2 * var(--gridSize)); +} + +.card-plan:last-child { + margin-right: 0; +} + +.card-plan-actionable { + cursor: pointer; + transition: all 0.2s ease; +} + +.card-plan-actionable:focus { + outline: none; +} + +.card-plan-actionable:not(.disabled):hover { + box-shadow: var(--defaultShadow); + transform: translateY(-2px); +} + +.card-plan-actionable.selected { + border-color: var(--darkBlue); +} + +.card-plan-actionable.disabled { + cursor: not-allowed; + background-color: var(--disableGrayBg); + border-color: var(--disableGrayBorder); +} + +.card-plan-actionable.disabled h2, +.card-plan-actionable.disabled ul { + color: var(--disableGrayText); +} + +.card-plan-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0; +} + +.card-plan-price { + font-size: var(--bigFontSize); +} + +.card-plan-body { + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize)); +} + +.card-plan-body ul > li { + margin-bottom: calc(var(--gridSize) / 2); +} + +.card-plan-body .alert { + margin-bottom: 0; +} + +.card-plan-recommended { + position: relative; + padding: 6px calc(var(--gridSize) * 2); + left: -1px; + bottom: -1px; + width: 450px; + color: #fff; + background-color: var(--darkBlue); + border-radius: 0 0 3px 3px; + box-sizing: border-box; + font-size: var(--smallFontSize); +} diff --git a/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx b/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx new file mode 100644 index 00000000000..937d470a2d8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx @@ -0,0 +1,167 @@ +/* + * 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. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; +import CheckIcon from '../../../components/icons-components/CheckIcon'; +import RecommendedIcon from '../../../components/icons-components/RecommendedIcon'; +import { Alert } from '../../../components/ui/Alert'; +import { formatPrice } from '../organization/utils'; +import { translate } from '../../../helpers/l10n'; +import * as theme from '../../../app/theme'; +import './CardPlan.css'; + +interface Props { + className?: string; + disabled?: boolean; + onClick?: () => void; + selected?: boolean; + startingPrice?: string; +} + +interface CardProps extends Props { + children: React.ReactNode; + recommended?: string; + title: string; +} + +export default function CardPlan(props: CardProps) { + const { className, disabled, onClick, recommended, selected, startingPrice } = props; + const isActionable = Boolean(onClick); + return ( +
+

+ + {isActionable && ( + + )} + {props.title} + + {startingPrice ? ( + {startingPrice} + }} + /> + ) : ( + {formatPrice(0)} + )} +

+
{props.children}
+ {recommended && ( +
+ + {translate('recommended')} }} + /> +
+ )} +
+ ); +} + +interface FreeProps extends Props { + almName?: string; + hasWarning: boolean; +} + +export function FreeCardPlan({ almName, hasWarning, ...props }: FreeProps) { + const showInfo = almName && props.disabled; + const showWarning = almName && hasWarning && !props.disabled; + + return ( + + <> +
    +
  • {translate('billing.free_plan.all_projects_analyzed_public')}
  • +
  • {translate('billing.free_plan.anyone_can_browse_source_code')}
  • +
+ {showWarning && ( + + + + )} + {showInfo && ( + + + + )} + +
+ ); +} + +interface PaidProps extends Props { + isRecommended: boolean; +} + +export function PaidCardPlan({ isRecommended, ...props }: PaidProps) { + const advantages = [ + translate('billing.upgrade_box.unlimited_private_projects'), + translate('billing.upgrade_box.strict_control_private_data'), + translate('billing.upgrade_box.cancel_anytime'), + translate('billing.upgrade_box.free_trial') + ]; + + return ( + + <> +
    + {advantages.map((text, idx) => ( +
  • + + {text} +
  • + ))} +
+
+ + {translate('learn_more')} + +
+ +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx new file mode 100644 index 00000000000..2190a5073db --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx @@ -0,0 +1,71 @@ +/* + * 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. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import CardPlan, { FreeCardPlan, PaidCardPlan } from '../CardPlan'; +import { click } from '../../../../helpers/testUtils'; + +it('should render correctly', () => { + expect( + shallow( + +
content
+
+ ) + ).toMatchSnapshot(); +}); + +it('should be actionable', () => { + const onClick = jest.fn(); + const wrapper = shallow( + +
content
+
+ ); + + expect(wrapper).toMatchSnapshot(); + click(wrapper); + wrapper.setProps({ selected: true }); + expect(wrapper).toMatchSnapshot(); +}); + +describe('#FreeCardPlan', () => { + it('should render correctly', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('should render with warning', () => { + expect( + shallow() + ).toMatchSnapshot(); + }); + + it('should render disabled with info', () => { + expect( + shallow() + ).toMatchSnapshot(); + }); +}); + +describe('#PaidCardPlan', () => { + it('should render correctly', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap new file mode 100644 index 00000000000..8de24b338b4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap @@ -0,0 +1,294 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#FreeCardPlan should render correctly 1`] = ` + +
    +
  • + billing.free_plan.all_projects_analyzed_public +
  • +
  • + billing.free_plan.anyone_can_browse_source_code +
  • +
+
+`; + +exports[`#FreeCardPlan should render disabled with info 1`] = ` + +
    +
  • + billing.free_plan.all_projects_analyzed_public +
  • +
  • + billing.free_plan.anyone_can_browse_source_code +
  • +
+ + + +
+`; + +exports[`#FreeCardPlan should render with warning 1`] = ` + +
    +
  • + billing.free_plan.all_projects_analyzed_public +
  • +
  • + billing.free_plan.anyone_can_browse_source_code +
  • +
+ + + +
+`; + +exports[`#PaidCardPlan should render correctly 1`] = ` + +
    +
  • + + billing.upgrade_box.unlimited_private_projects +
  • +
  • + + billing.upgrade_box.strict_control_private_data +
  • +
  • + + billing.upgrade_box.cancel_anytime +
  • +
  • + + billing.upgrade_box.free_trial +
  • +
+
+ + learn_more + +
+
+`; + +exports[`should be actionable 1`] = ` +
+

+ + + Free Plan + + + billing.price_format.0 + +

+
+
+ content +
+
+
+`; + +exports[`should be actionable 2`] = ` +
+

+ + + Free Plan + + + billing.price_format.0 + +

+
+
+ content +
+
+
+`; + +exports[`should render correctly 1`] = ` +
+

+ + Paid Plan + + + $10 + , + } + } + /> +

+
+
+ content +
+
+
+ + + recommended + , + } + } + /> +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx index 85627bed7ca..11f6fe3fbc5 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx @@ -172,10 +172,11 @@ export default class AutoOrganizationCreate extends React.PureComponent diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx index fb903669336..ec5942f3925 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx @@ -108,9 +108,10 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent {subscriptionPlans !== undefined && ( diff --git a/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx index 45afee9f63c..ce7d8b6f9f3 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx @@ -31,7 +31,6 @@ interface Props { handleOrgDetailsFinish: (organization: T.Organization) => Promise; handleOrgDetailsStepOpen: () => void; onDone: () => void; - onlyPaid?: boolean; organization?: T.Organization; step: Step; subscriptionPlans?: T.SubscriptionPlan[]; @@ -67,7 +66,6 @@ export default class ManualOrganizationCreate extends React.PureComponent createOrganization={this.handleCreateOrganization} onDone={this.props.onDone} onUpgradeFail={this.props.onUpgradeFail} - onlyPaid={this.props.onlyPaid} open={this.props.step === Step.Plan} subscriptionPlans={subscriptionPlans} /> diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx index 0c9e8dc961a..25a9964d360 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx @@ -18,10 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Link } from 'react-router'; -import Radio from '../../../components/controls/Radio'; +import { FreeCardPlan, PaidCardPlan } from '../components/CardPlan'; import { translate } from '../../../helpers/l10n'; +import { AlmOrganization, AlmApplication } from '../../../app/types'; export enum Plan { Free = 'free', @@ -29,6 +28,8 @@ export enum Plan { } interface Props { + almApplication?: AlmApplication; + almOrganization?: AlmOrganization; onChange: (plan: Plan) => void; plan: Plan; startingPrice: string; @@ -44,43 +45,36 @@ export default class PlanSelect extends React.PureComponent { }; render() { - const { plan } = this.props; + const { almApplication, almOrganization, plan } = this.props; + const hasPrivateRepo = Boolean(almOrganization && almOrganization.privateRepos > 0); + const onlyPrivateRepo = Boolean( + hasPrivateRepo && almOrganization && almOrganization.publicRepos === 0 + ); + + const cards = [ + , + + ]; + return (
-
- - {translate('billing.free_plan.title')} - -

- {translate('billing.free_plan.description')} -

-
-
- - {translate('billing.paid_plan.title')} - -

- - {' '} - - {translate('learn_more')} - -
- - ) - }} - /> -

-
+ {hasPrivateRepo ? cards : cards.reverse()}
); } diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx index 59009a44b98..adf99b376d2 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx @@ -21,20 +21,21 @@ import * as React from 'react'; import BillingFormShim from './BillingFormShim'; import PlanSelect, { Plan } from './PlanSelect'; import { formatPrice } from './utils'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; import Step from '../../tutorials/components/Step'; +import { SubmitButton } from '../../../components/ui/buttons'; import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; -import { translate } from '../../../helpers/l10n'; import { getExtensionStart } from '../../../app/components/extensions/utils'; -import { SubmitButton } from '../../../components/ui/buttons'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { translate } from '../../../helpers/l10n'; const BillingForm = withCurrentUser(BillingFormShim); interface Props { + almApplication?: T.AlmApplication; + almOrganization?: T.AlmOrganization; createOrganization: () => Promise; onDone: () => void; onUpgradeFail?: () => void; - onlyPaid?: boolean; open: boolean; subscriptionPlans: T.SubscriptionPlan[]; } @@ -51,7 +52,7 @@ export default class PlanStep extends React.PureComponent { constructor(props: Props) { super(props); this.state = { - plan: props.onlyPaid ? Plan.Paid : Plan.Free, + plan: props.almOrganization && props.almOrganization.privateRepos > 0 ? Plan.Paid : Plan.Free, ready: false, submitting: false }; @@ -100,13 +101,13 @@ export default class PlanStep extends React.PureComponent {
{this.state.ready && ( <> - {!this.props.onlyPaid && ( - - )} + {this.state.plan === Plan.Paid ? ( { }; render() { + const { almOrganization } = this.props; const stepTitle = translate( - this.props.onlyPaid + almOrganization && almOrganization.privateRepos > 0 && almOrganization.publicRepos === 0 ? 'onboarding.create_organization.enter_payment_details' : 'onboarding.create_organization.choose_plan' ); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx index d4c4ed73bc8..c7999e6fa19 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx @@ -33,6 +33,8 @@ const organization = { description: 'description-foo', key: 'key-foo', name: 'name-foo', + privateRepos: 0, + publicRepos: 3, url: 'http://example.com/foo' }; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx index fbb17c78fe2..2fb3e40e6a7 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx @@ -30,6 +30,8 @@ const almOrganization = { key: 'key-foo', name: 'name-foo', personal: true, + privateRepos: 0, + publicRepos: 3, url: 'http://example.com/foo' }; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx index 59cc3a8139c..4da19584a2e 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx @@ -55,6 +55,8 @@ jest.mock('../../../../api/alm-integration', () => ({ key: 'sonarsource', name: 'SonarSource', personal: false, + privateRepos: 0, + publicRepos: 3, url: 'https://www.sonarsource.com' } }), @@ -79,11 +81,22 @@ const user: T.LoggedInUser = { showOnboardingTutorial: false }; -const almOrganization = { +const fooAlmOrganization = { + avatar: 'my-avatar', + key: 'foo', + name: 'Foo', + personal: true, + privateRepos: 0, + publicRepos: 3 +}; + +const fooBarAlmOrganization = { avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', key: 'Foo&Bar', name: 'Foo & Bar', - personal: true + personal: true, + privateRepos: 0, + publicRepos: 3 }; const boundOrganization = { key: 'foobar', name: 'Foo & Bar' }; @@ -129,12 +142,7 @@ it('should render with auto tab selected and manual disabled', async () => { it('should render with auto personal organization bind page', async () => { (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ - almOrganization: { - key: 'foo', - name: 'Foo', - avatar: 'my-avatar', - personal: true - } + almOrganization: fooAlmOrganization }); const wrapper = shallowRender({ currentUser: { ...user, externalProvider: 'github', personalOrganization: 'foo' }, @@ -147,12 +155,7 @@ it('should render with auto personal organization bind page', async () => { it('should render with organization bind page', async () => { (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ - almOrganization: { - key: 'foo', - name: 'Foo', - avatar: 'my-avatar', - personal: false - } + almOrganization: { ...fooAlmOrganization, personal: false } }); const wrapper = shallowRender({ currentUser: { ...user, externalProvider: 'github' }, @@ -164,7 +167,9 @@ it('should render with organization bind page', async () => { }); it('should slugify and find a uniq organization key', async () => { - (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ almOrganization }); + (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ + almOrganization: fooBarAlmOrganization + }); (getOrganizations as jest.Mock).mockResolvedValueOnce({ organizations: [{ key: 'foo-and-bar' }, { key: 'foo-and-bar-1' }] }); @@ -247,9 +252,7 @@ it('should redirect to projects creation page after creation', async () => { state: { organization: 'foo', tab: 'manual' } }); - wrapper.setState({ - almOrganization: { key: 'foo', name: 'Foo', avatar: 'my-avatar', personal: false } - }); + wrapper.setState({ almOrganization: { ...fooAlmOrganization, personal: false } }); (get as jest.Mock).mockReturnValueOnce(Date.now().toString()); wrapper.instance().handleOrgCreated('foo'); expect(push).toHaveBeenCalledWith({ @@ -260,7 +263,7 @@ it('should redirect to projects creation page after creation', async () => { it('should display AutoOrganizationCreate with already bound organization', async () => { (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ - almOrganization: { ...almOrganization, personal: false }, + almOrganization: { ...fooBarAlmOrganization, personal: false }, boundOrganization }); (get as jest.Mock).mockReturnValueOnce(Date.now().toString()); @@ -283,7 +286,7 @@ it('should display AutoOrganizationCreate with already bound organization', asyn it('should redirect to org page when already bound and no binding in progress', async () => { (getAlmOrganization as jest.Mock).mockResolvedValueOnce({ - almOrganization, + almOrganization: fooBarAlmOrganization, boundOrganization }); const push = jest.fn(); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx index 3e7736019ed..de39d626ab5 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx @@ -47,15 +47,6 @@ it('should render and create organization', async () => { expect(wrapper).toMatchSnapshot(); }); -it('should preselect paid plan', async () => { - const wrapper = shallowRender({ onlyPaid: true }); - - await waitAndUpdate(wrapper); - wrapper.find('OrganizationDetailsForm').prop('onContinue')(organization); - await waitAndUpdate(wrapper); - expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true); -}); - function shallowRender(props: Partial = {}) { return shallow( { const onChange = jest.fn(); - const wrapper = shallow(); + const wrapper = shallowRender({ onChange }); expect(wrapper).toMatchSnapshot(); - wrapper.find('Radio[checked=false]').prop('onCheck')(); + click(wrapper.find('PaidCardPlan')); expect(onChange).toBeCalledWith(Plan.Paid); - wrapper.setProps({ plan: Plan.Paid }); expect(wrapper).toMatchSnapshot(); }); + +it('should recommend paid plan', () => { + const wrapper = shallowRender({ + almOrganization: { key: 'foo', name: 'Foo', personal: false, privateRepos: 1, publicRepos: 5 }, + plan: Plan.Paid + }); + expect(wrapper.find('PaidCardPlan').prop('isRecommended')).toBe(true); + expect(wrapper.find('FreeCardPlan').prop('disabled')).toBe(false); + expect(wrapper.find('FreeCardPlan').prop('hasWarning')).toBe(false); + + wrapper.setProps({ plan: Plan.Free }); + expect(wrapper.find('FreeCardPlan').prop('hasWarning')).toBe(true); +}); + +it('should recommend paid plan and disable free plan', () => { + const wrapper = shallowRender({ + almOrganization: { key: 'foo', name: 'Foo', personal: false, privateRepos: 1, publicRepos: 0 } + }); + expect(wrapper.find('PaidCardPlan').prop('isRecommended')).toBe(true); + expect(wrapper.find('FreeCardPlan').prop('disabled')).toBe(true); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx index 2334b42871f..d5ff1e778df 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx @@ -80,15 +80,21 @@ it('should upgrade', async () => { it('should preselect paid plan', async () => { const wrapper = shallow( ); await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); expect(wrapper.dive()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx index 86b68fd9370..d4e2edb95ed 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx @@ -51,7 +51,14 @@ it('should display already bound alert message', () => { expect( shallowRender({ almInstallId: 'foo', - almOrganization: { avatar: 'foo-avatar', key: 'foo', name: 'Foo', personal: false }, + almOrganization: { + avatar: 'foo-avatar', + key: 'foo', + name: 'Foo', + personal: false, + privateRepos: 0, + publicRepos: 3 + }, boundOrganization: { avatar: 'bound-avatar', key: 'bound', name: 'Bound' } }).find('Alert') ).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap index 91da2875d70..f268d88ce0f 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap @@ -57,10 +57,30 @@ exports[`should display choice between import or creation 1`] = `
-
- - - billing.free_plan.title - - -

- billing.free_plan.description -

-
-
- - - billing.paid_plan.title - - -

- - - - learn_more - -
- , - "price": "10", - } - } - /> -

-
+ + `; exports[`should render and select 2`] = `
-
- - - billing.free_plan.title - - -

- billing.free_plan.description -

-
-
- - - billing.paid_plan.title - - -

- - - - learn_more - -
- , - "price": "10", - } - } - /> -

-
+ +
`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap index b03f3c1ef55..a979c50685f 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap @@ -1,18 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should preselect paid plan 1`] = ` - -`; - -exports[`should preselect paid plan 2`] = `
@@ -34,6 +22,21 @@ exports[`should preselect paid plan 2`] = `
+ + + + ); +} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts index cfaba659431..d73e88fcbeb 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts @@ -36,8 +36,12 @@ it('#isVSTS', () => { }); it('#isPersonal', () => { - expect(isPersonal({ key: 'foo', name: 'Foo', personal: true })).toBeTruthy(); - expect(isPersonal({ key: 'foo', name: 'Foo', personal: false })).toBeFalsy(); + expect( + isPersonal({ key: 'foo', name: 'Foo', personal: true, privateRepos: 0, publicRepos: 3 }) + ).toBeTruthy(); + expect( + isPersonal({ key: 'foo', name: 'Foo', personal: false, privateRepos: 0, publicRepos: 3 }) + ).toBeFalsy(); }); it('#sanitizeAlmId', () => { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 2b4196f9e2a..ea32be1e07b 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -132,6 +132,7 @@ quality_profile=Quality Profile raw=Raw recent_history=Recent History recently_browsed=Recently Browsed +recommended=Recommended refresh=Refresh reload=Reload remove=Remove -- 2.39.5