Этот коммит содержится в:
Conor Macpherson
2023-03-28 16:15:14 -04:00
родитель 5df7e62f8a
Коммит 58fead7d9d
4 изменённых файлов: 64 добавлений и 19 удалений

Просмотреть файл

@@ -88,18 +88,18 @@
font-size: 12px; font-size: 12px;
} }
.totalCost { .totalCostWarning {
width: 141px; width: 141px;
} }
.totalCost > span:first-child { .totalCostWarning > span:first-child {
color: var(--sys-denim-center-channel-text); color: var(--sys-denim-center-channel-text);
font-family: 'Open Sans'; font-family: 'Open Sans';
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
} }
.totalCost > span:last-child { .totalCostWarning > span:last-child {
color: rgba(var(--sys-denim-center-channel-text-rgb), 0.72); color: rgba(var(--sys-denim-center-channel-text-rgb), 0.72);
font-family: 'Open Sans'; font-family: 'Open Sans';
font-size: 12px; font-size: 12px;

Просмотреть файл

@@ -18,7 +18,6 @@ import {findSelfHostedProductBySku} from 'utils/hosted_customer';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
const MONTHS_IN_YEAR = 12; const MONTHS_IN_YEAR = 12;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
const MAX_TRANSACTION_VALUE = 1_000_000 - 1; const MAX_TRANSACTION_VALUE = 1_000_000 - 1;
interface Props { interface Props {
@@ -35,14 +34,14 @@ export default function SelfHostedExpansionCard(props: Props) {
const endsAt = moment(parseInt(license.ExpiresAt, 10)).format('MMM. D, YYYY'); const endsAt = moment(parseInt(license.ExpiresAt, 10)).format('MMM. D, YYYY');
const [additionalSeats, setAdditionalSeats] = useState(props.initialSeats); const [additionalSeats, setAdditionalSeats] = useState(props.initialSeats);
const [overMaxSeats, setOverMaxSeats] = useState(false); const [overMaxSeats, setOverMaxSeats] = useState(false);
const licenseExpiry = parseInt(license.ExpiresAt, 10); const licenseExpiry = new Date(parseInt(license.ExpiresAt, 10));
const invalidAdditionalSeats = additionalSeats === 0 || isNaN(additionalSeats); const invalidAdditionalSeats = additionalSeats === 0 || isNaN(additionalSeats);
const [products] = useGetSelfHostedProducts(); const [products] = useGetSelfHostedProducts();
const currentProduct = findSelfHostedProductBySku(products, license.SkuShortName); const currentProduct = findSelfHostedProductBySku(products, license.SkuShortName);
const getMonthsUntilExpiry = () => { const getMonthsUntilExpiry = () => {
const now = new Date(); const now = new Date();
return Math.ceil((licenseExpiry - now.getTime()) / MILLISECONDS_PER_DAY / 30); return (licenseExpiry.getMonth() - now.getMonth()) + 12 * (licenseExpiry.getFullYear() - now.getFullYear());
}; };
const getMonthlyPrice = () => { const getMonthlyPrice = () => {
@@ -209,7 +208,7 @@ export default function SelfHostedExpansionCard(props: Props) {
<br/> <br/>
<FormattedMessage <FormattedMessage
id='self_hosted_expansion_rhs_card_cost_per_user_breakdown' id='self_hosted_expansion_rhs_card_cost_per_user_breakdown'
defaultMessage='{costPerUser} x {monthsUntilExpiry} months' defaultMessage='${costPerUser} x {monthsUntilExpiry} months'
values={{ values={{
costPerUser: getMonthlyPrice().toFixed(2), costPerUser: getMonthlyPrice().toFixed(2),
monthsUntilExpiry: getMonthsUntilExpiry(), monthsUntilExpiry: getMonthsUntilExpiry(),
@@ -219,7 +218,7 @@ export default function SelfHostedExpansionCard(props: Props) {
<div className='costAmount'> <div className='costAmount'>
<span>{'$' + getCostPerUser().toFixed(2)}</span> <span>{'$' + getCostPerUser().toFixed(2)}</span>
</div> </div>
<div className='totalCost'> <div className='totalCostWarning'>
<FormattedMessage <FormattedMessage
id='self_hosted_expansion_rhs_card_total_title' id='self_hosted_expansion_rhs_card_total_title'
defaultMessage='Total' defaultMessage='Total'
@@ -230,7 +229,7 @@ export default function SelfHostedExpansionCard(props: Props) {
defaultMessage='The total will be prorated' defaultMessage='The total will be prorated'
/> />
</div> </div>
<span className='costAmount'> <span className='totalCostAmount'>
<span>{'$' + getTotal().toFixed(2)}</span> <span>{'$' + getTotal().toFixed(2)}</span>
</span> </span>
</div> </div>

Просмотреть файл

@@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import {screen, fireEvent} from '@testing-library/react'; import {screen, fireEvent, waitFor} from '@testing-library/react';
import {GlobalState} from 'types/store'; import {GlobalState} from 'types/store';
@@ -11,7 +11,7 @@ import {SelfHostedSignupForm, SelfHostedSignupProgress} from '@mattermost/types/
import {renderWithIntlAndStore} from 'tests/react_testing_utils'; import {renderWithIntlAndStore} from 'tests/react_testing_utils';
import {TestHelper as TH} from 'utils/test_helper'; import {TestHelper as TH} from 'utils/test_helper';
import {SelfHostedProducts, ModalIdentifiers} from 'utils/constants'; import {SelfHostedProducts, ModalIdentifiers, RecurringIntervals} from 'utils/constants';
import {DeepPartial} from '@mattermost/types/utilities'; import {DeepPartial} from '@mattermost/types/utilities';
@@ -76,6 +76,7 @@ const mockProfessionalProduct = TH.getProductMock({
name: 'Professional', name: 'Professional',
sku: SelfHostedProducts.PROFESSIONAL, sku: SelfHostedProducts.PROFESSIONAL,
price_per_seat: 7.5, price_per_seat: 7.5,
recurring_interval: RecurringIntervals.MONTH
}); });
jest.mock('mattermost-redux/client', () => { jest.mock('mattermost-redux/client', () => {
@@ -129,6 +130,11 @@ jest.mock('utils/hosted_customer', () => {
const productName = SelfHostedProducts.PROFESSIONAL; const productName = SelfHostedProducts.PROFESSIONAL;
// Licensed expiry set as 3 months from the current date (rolls over to new years).
const licenseExpiry = new Date();
const monthsUntilLicenseExpiry = 3;
licenseExpiry.setMonth(licenseExpiry.getMonth() + monthsUntilLicenseExpiry);
const initialState: DeepPartial<GlobalState> = { const initialState: DeepPartial<GlobalState> = {
views: { views: {
modals: { modals: {
@@ -143,11 +149,6 @@ const initialState: DeepPartial<GlobalState> = {
storage: {}, storage: {},
}, },
entities: { entities: {
admin: {
analytics: {
TOTAL_USERS: existingUsers,
},
},
teams: { teams: {
currentTeamId: '', currentTeamId: '',
}, },
@@ -163,6 +164,7 @@ const initialState: DeepPartial<GlobalState> = {
license: { license: {
Sku: productName, Sku: productName,
Users: '50', Users: '50',
ExpiresAt: licenseExpiry.getTime().toString()
}, },
}, },
cloud: { cloud: {
@@ -224,6 +226,7 @@ interface PurchaseForm {
state: string; state: string;
zip: string; zip: string;
seats: string; seats: string;
agree: boolean;
} }
const defaultSuccessForm: PurchaseForm = { const defaultSuccessForm: PurchaseForm = {
@@ -236,6 +239,7 @@ const defaultSuccessForm: PurchaseForm = {
state: 'MN', state: 'MN',
zip: '55423', zip: '55423',
seats: '10', seats: '10',
agree: true,
}; };
function fillForm(form: PurchaseForm) { function fillForm(form: PurchaseForm) {
@@ -248,6 +252,9 @@ function fillForm(form: PurchaseForm) {
selectDropdownValue('selfHostedExpansionStateSelector', form.state); selectDropdownValue('selfHostedExpansionStateSelector', form.state);
changeByPlaceholder('Zip/Postal Code', form.zip); changeByPlaceholder('Zip/Postal Code', form.zip);
changeByTestId('seatsInput', form.seats); changeByTestId('seatsInput', form.seats);
if (form.agree) {
fireEvent.click(screen.getByText('I have read and agree', {exact: false}));
}
expect(document.getElementsByClassName('SelfHostedExpansionRHSCard__AddSeatsWarning')[0] as HTMLElement).toBeEnabled(); expect(document.getElementsByClassName('SelfHostedExpansionRHSCard__AddSeatsWarning')[0] as HTMLElement).toBeEnabled();
@@ -263,7 +270,7 @@ function fillForm(form: PurchaseForm) {
return completeButton; return completeButton;
} }
describe('SelfHostedExpansionModal', () => { describe('SelfHostedExpansionModal Open', () => {
it('renders the form', () => { it('renders the form', () => {
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState); renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
@@ -321,7 +328,45 @@ describe('SelfHostedExpansionModal', () => {
// }); // });
}); });
describe('SelfHostedExpansionModal :: canSubmit', () => { describe('SelfHostedExpansionModal RHS Card', () => {
it("New seats input should be pre-populated with the difference from the active users and licensed seats", () => {
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
const expectedPrePopulatedSeats = (initialState.entities?.users?.filteredStats?.total_users_count || 1) - parseInt(initialState.entities?.general?.license?.Users || '0', 10);
const seatsField = screen.getByTestId('seatsInput').querySelector('input');
expect(seatsField).toBeInTheDocument();
expect(seatsField?.value).toBe(expectedPrePopulatedSeats.toString());
});
it("Cost per User should be represented as the current subscription price multiplied by the remaining months", () => {
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
const expectedCostPerUser = monthsUntilLicenseExpiry * mockProfessionalProduct.price_per_seat;
const costPerUser = document.getElementsByClassName('costPerUser')[0];
expect(costPerUser).toBeInTheDocument();
expect(costPerUser.innerHTML).toContain('Cost per user<br>$' + mockProfessionalProduct.price_per_seat.toFixed(2) + ' x ' + monthsUntilLicenseExpiry + ' months');
const costAmount = document.getElementsByClassName('costAmount')[0];
expect(costAmount).toBeInTheDocument();
expect(costAmount.innerHTML).toContain('$' + expectedCostPerUser)
});
it("Total cost User should be represented as the current subscription price multiplied by the remaining months multiplied by the number of users", () => {
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
const seatsInputValue = 100;
changeByTestId('seatsInput', seatsInputValue.toString());
const expectedTotalCost = monthsUntilLicenseExpiry * mockProfessionalProduct.price_per_seat * seatsInputValue;
const costAmount = document.getElementsByClassName('totalCostAmount')[0];
expect(costAmount).toBeInTheDocument();
expect(costAmount.innerHTML).toContain('$' + expectedTotalCost)
});
});
describe('SelfHostedExpansionModal Submit', () => {
function makeHappyPathState(): FormState { function makeHappyPathState(): FormState {
return { return {
address: 'string', address: 'string',
@@ -366,7 +411,6 @@ describe('SelfHostedExpansionModal :: canSubmit', () => {
expect(canSubmit(state, SelfHostedSignupProgress.PAID)).toBe(true); expect(canSubmit(state, SelfHostedSignupProgress.PAID)).toBe(true);
}); });
// TODO: Needed?
it('if created subscription, can submit', () => { it('if created subscription, can submit', () => {
const state = makeInitialState(1); const state = makeInitialState(1);
state.submitting = false; state.submitting = false;

Просмотреть файл

@@ -409,6 +409,7 @@ export default function SelfHostedExpansionModal() {
/> />
</span> </span>
<Address <Address
testPrefix='selfHostedExpansion'
type='billing' type='billing'
country={formState.country} country={formState.country}
changeCountry={(option) => { changeCountry={(option) => {
@@ -450,6 +451,7 @@ export default function SelfHostedExpansionModal() {
/> />
</div> </div>
<Address <Address
testPrefix='selfHostedExpansion'
type='shipping' type='shipping'
country={formState.shippingCountry} country={formState.shippingCountry}
changeCountry={(option) => { changeCountry={(option) => {