Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/shared/__tests__/fixture/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ export const createTestSettings = (
autoDismissNotifications: true,
optOutCompanion: true,
optOutReadingStreak: true,
optOutStreakFreeze: false,
toggleOptOutStreakFreeze: jest.fn(),
optOutLevelSystem: false,
optOutQuestSystem: false,
optOutAchievements: false,
isGamificationEnabled: true,
toggleOptOutAchievements: jest.fn(),
toggleAllGamification: jest.fn(),
sidebarExpanded: true,
companionExpanded: true,
sortingEnabled: true,
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/__tests__/helpers/boot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@ export const settingsContext: SettingsContextData = {
openNewTab: true,
optOutCompanion: false,
optOutReadingStreak: true,
optOutStreakFreeze: false,
optOutLevelSystem: false,
optOutQuestSystem: false,
optOutAchievements: false,
isGamificationEnabled: true,
toggleOptOutAchievements: jest.fn(),
toggleAllGamification: jest.fn(),
setSettings: jest.fn(),
setSpaciness: jest.fn(),
setTheme: jest.fn(),
Expand All @@ -60,6 +65,7 @@ export const settingsContext: SettingsContextData = {
toggleOpenNewTab: jest.fn(),
toggleOptOutCompanion: jest.fn(),
toggleOptOutReadingStreak: jest.fn(),
toggleOptOutStreakFreeze: jest.fn(),
toggleOptOutLevelSystem: jest.fn(),
toggleOptOutQuestSystem: jest.fn(),
toggleShowTopSites: jest.fn(),
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/components/modals/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ const StreakRecoverModal = dynamic(
),
);

const StreakFreezePurchaseModal = dynamic(
() =>
import(
/* webpackChunkName: "streakFreezePurchaseModal" */ './streaks/StreakFreezePurchaseModal'
),
);

const SlackIntegrationModal = dynamic(
() =>
import(
Expand Down Expand Up @@ -544,6 +551,7 @@ export const modals = {
[LazyModal.TopMembers]: TopMembersModal,
[LazyModal.BookmarkReminder]: BookmarkReminderModal,
[LazyModal.RecoverStreak]: StreakRecoverModal,
[LazyModal.StreakFreezePurchase]: StreakFreezePurchaseModal,
[LazyModal.SlackIntegration]: SlackIntegrationModal,
[LazyModal.ReportSource]: ReportSourceModal,
[LazyModal.UserFollowersModal]: UserFollowersModal,
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/components/modals/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export enum LazyModal {
ImageView = 'imageView',
NewStreak = 'newStreak',
RecoverStreak = 'recoverStreak',
StreakFreezePurchase = 'streakFreezePurchase',
ReputationPrivileges = 'reputationPrivileges',
MarketingCta = 'marketingCta',
Share = 'share',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useAuthContext } from '../../../contexts/AuthContext';
import { useActions } from '../../../hooks';
import { ActionType } from '../../../graphql/actions';
import StreakReminderSwitch from '../../streak/StreakReminderSwitch';
import { StreakFreezeUpsell } from '../../streak/StreakFreezeUpsell';

const Paragraph = classed('p', 'text-center text-text-tertiary');

Expand Down Expand Up @@ -120,6 +121,9 @@ export default function NewStreakModal({
? 'Epic win! You are in a league of your own'
: `New milestone reached! You are unstoppable.`}
</Paragraph>
<StreakFreezeUpsell className="mt-6">
Protect your streak with streak freezes
</StreakFreezeUpsell>
<Checkbox
name="show_streaks"
className="mt-10"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { QueryClient } from '@tanstack/react-query';
import ReactModal from 'react-modal';
import { TestBootProvider } from '../../../../__tests__/helpers/boot';
import { mockGraphQL } from '../../../../__tests__/helpers/graphql';
import { waitForNock } from '../../../../__tests__/helpers/utilities';
import loggedUser from '../../../../__tests__/fixture/loggedUser';
import { USER_STREAK_QUERY } from '../../../graphql/users';
import {
PURCHASE_STREAK_FREEZE_MUTATION,
STREAK_FREEZE_PRODUCTS_QUERY,
USER_STREAK_FREEZE_DATES_QUERY,
} from '../../../graphql/streakFreeze';
import { DayOfWeek } from '../../../lib/date';
import { StreakFreezePurchaseModal } from './StreakFreezePurchaseModal';

ReactModal.setAppElement('body');

const products = [
{
id: 'p1',
name: '1 pack',
value: 100,
image: 'img1',
flags: { quantity: 1 },
},
{
id: 'p3',
name: '3 pack',
value: 250,
image: 'img3',
flags: { quantity: 3 },
},
{
id: 'p5',
name: '5 pack',
value: 400,
image: 'img5',
flags: { quantity: 5 },
},
];

const mockStreakQuery = (freezesAvailable = 0) => {
mockGraphQL({
request: { query: USER_STREAK_QUERY },
result: {
data: {
userStreak: {
max: 5,
total: 5,
current: 5,
weekStart: DayOfWeek.Monday,
lastViewAt: new Date().toISOString(),
freezesAvailable,
},
},
},
});
};

const mockProductsQuery = () => {
mockGraphQL({
request: { query: STREAK_FREEZE_PRODUCTS_QUERY },
result: { data: { streakFreezeProducts: products } },
});
};

const mockFreezeDatesQuery = () => {
mockGraphQL({
request: { query: USER_STREAK_FREEZE_DATES_QUERY, variables: {} },
result: { data: { userStreakFreezeDates: [] } },
});
};

const renderComponent = ({
balance = 1000,
freezesAvailable = 3,
}: { balance?: number; freezesAvailable?: number } = {}) => {
const client = new QueryClient();
mockStreakQuery(freezesAvailable);
mockProductsQuery();
mockFreezeDatesQuery();

return render(
<TestBootProvider
client={client}
auth={{ user: { ...loggedUser, balance: { amount: balance } } }}
>
<StreakFreezePurchaseModal isOpen onRequestClose={jest.fn()} />
</TestBootProvider>,
);
};

describe('StreakFreezePurchaseModal', () => {
it('should render the packs once loaded', async () => {
renderComponent();

await waitForNock();

expect(
await screen.findByTestId('streak-freeze-pack-p1'),
).toHaveTextContent('1 freeze');
expect(screen.getByTestId('streak-freeze-pack-p3')).toHaveTextContent(
'3 freezes',
);
expect(screen.getByTestId('streak-freeze-pack-p5')).toHaveTextContent(
'5 freezes',
);
});

it('should disable packs that would push the user over the cap of 5', async () => {
renderComponent({ freezesAvailable: 3 });

await waitForNock();

// 3 available + 1 = 4 (ok), + 3 = 6 (disabled), + 5 = 8 (disabled)
expect(await screen.findByTestId('streak-freeze-pack-p1')).toBeEnabled();
expect(screen.getByTestId('streak-freeze-pack-p3')).toBeDisabled();
expect(screen.getByTestId('streak-freeze-pack-p5')).toBeDisabled();
});

it('should show "Buy Cores" when the balance cannot cover the selected pack', async () => {
renderComponent({ balance: 0, freezesAvailable: 0 });

await waitForNock();

await waitFor(() =>
expect(
screen.getByTestId('streak-freeze-purchase-button'),
).toHaveTextContent('Buy Cores100'),
);
});

it('should purchase the selected pack', async () => {
renderComponent({ balance: 1000, freezesAvailable: 0 });

await waitForNock();

let mutationCalled = false;
mockGraphQL({
request: {
query: PURCHASE_STREAK_FREEZE_MUTATION,
variables: { productId: 'p1' },
},
result: () => {
mutationCalled = true;

return {
data: {
purchaseStreakFreeze: {
freezesAvailable: 1,
balance: { amount: 900 },
transactionId: 't1',
},
},
};
},
});

const button = await screen.findByTestId('streak-freeze-purchase-button');
await waitFor(() =>
expect(button).toHaveTextContent('Get 1 freeze for 100'),
);
fireEvent.click(button);

await waitFor(() => expect(mutationCalled).toBeTruthy());
});
});
Loading
Loading