617 lines
21 KiB
TypeScript
617 lines
21 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import '@testing-library/jest-dom/vitest';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { cleanup, render, screen, waitFor, within } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { HttpResponse, http } from 'msw';
|
|
import { setupServer } from 'msw/node';
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
import { GrowthPage } from '../../apps/dashboard/src/features/growth/growth-page.js';
|
|
|
|
const ACCOUNT_ID = '@t2_grower:reddit.com';
|
|
|
|
function settings(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
enabled: true,
|
|
trustLevel: 1,
|
|
dailyAllowance: 3,
|
|
adaptiveAllowance: 3,
|
|
dailyLimit: null,
|
|
minDelayMs: 3_600_000,
|
|
jitterMs: 900_000,
|
|
quietHours: { enabled: true, startHour: 23, endHour: 7 },
|
|
allowNsfw: false,
|
|
pausedReason: null,
|
|
pausedAt: null,
|
|
consecutiveRemovals: 0,
|
|
levelChangedAt: null,
|
|
updatedAt: 1_700_000_000_000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function stats(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
postedLast24h: 1,
|
|
postedTotal: 6,
|
|
removedTotal: 1,
|
|
medianScore: 4,
|
|
queued: 2,
|
|
rejectedTotal: 3,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function browsing(includeRecent = false) {
|
|
return {
|
|
settings: {
|
|
enabled: false,
|
|
dailyTargetMin: 3,
|
|
dailyTargetMax: 6,
|
|
postWeight: 60,
|
|
commentWeight: 40,
|
|
quietHours: { enabled: true, startHour: 23, endHour: 7 },
|
|
minimumGapMs: 2_700_000,
|
|
jitterMs: 900_000,
|
|
selectionsPerSession: 1,
|
|
allowNsfw: false,
|
|
autoVoteEnabled: false,
|
|
nextDueAt: null,
|
|
updatedAt: 1_700_000_000_000,
|
|
},
|
|
todayTarget: 4,
|
|
stats: { selected: 0, posts: 0, comments: 0, rejected: 0, skipped: 0 },
|
|
noVotesSubmitted: true,
|
|
...(includeRecent ? { recentDecisions: [] } : {}),
|
|
};
|
|
}
|
|
|
|
function summary(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
accountId: ACCOUNT_ID,
|
|
redditUsername: 'grower',
|
|
settings: settings(),
|
|
stats: stats(),
|
|
subredditCount: 2,
|
|
activeSubredditCount: 1,
|
|
browsing: browsing(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function detail(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
...summary(),
|
|
subreddits: [
|
|
{
|
|
subreddit: 'AskUK',
|
|
enabled: true,
|
|
weight: 1,
|
|
dailyCap: 2,
|
|
disabledReason: null,
|
|
cooldownUntil: null,
|
|
addedAt: 1,
|
|
postedLast24h: 1,
|
|
},
|
|
{
|
|
subreddit: 'cats',
|
|
enabled: false,
|
|
weight: 1,
|
|
dailyCap: 3,
|
|
disabledReason: 'banned (r/cats)',
|
|
cooldownUntil: null,
|
|
addedAt: 2,
|
|
postedLast24h: 0,
|
|
},
|
|
],
|
|
recentComments: [
|
|
{
|
|
postId: 'p1',
|
|
subreddit: 'AskUK',
|
|
status: 'posted',
|
|
threadTitle: 'Best way to get to Leeds',
|
|
threadUrl: 'https://www.reddit.com/r/AskUK/comments/p1/',
|
|
comment: 'the train is usually quicker than the coach at that time of day',
|
|
commentPermalink: '/r/AskUK/comments/p1/_/c1/',
|
|
score: 7,
|
|
removed: false,
|
|
shadowBanned: null,
|
|
postedAt: 1_700_000_000_000,
|
|
discoveredAt: 1_699_000_000_000,
|
|
lastError: null,
|
|
},
|
|
{
|
|
postId: 'p2',
|
|
subreddit: 'AskUK',
|
|
status: 'rejected',
|
|
threadTitle: 'A thread that got discarded',
|
|
threadUrl: 'https://www.reddit.com/r/AskUK/comments/p2/',
|
|
comment: 'check out my profile for more',
|
|
commentPermalink: null,
|
|
score: null,
|
|
removed: false,
|
|
shadowBanned: null,
|
|
postedAt: null,
|
|
discoveredAt: 1_699_000_001_000,
|
|
lastError: 'Comment solicits contact or promotes a profile',
|
|
},
|
|
],
|
|
browsing: browsing(true),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
let overviewPayload: Record<string, unknown>[] = [];
|
|
let detailPayload: Record<string, unknown> = detail();
|
|
const requests: Array<{ method: string; url: string; body: unknown }> = [];
|
|
|
|
const server = setupServer(
|
|
http.get('/api/growth', () => HttpResponse.json({
|
|
generatedAt: Date.now(),
|
|
accounts: overviewPayload,
|
|
})),
|
|
http.get('/api/growth/:accountId', () => HttpResponse.json({
|
|
success: true,
|
|
account: detailPayload,
|
|
})),
|
|
http.patch('/api/growth/:accountId', async ({ request }) => {
|
|
requests.push({ method: 'PATCH', url: request.url, body: await request.json() });
|
|
return HttpResponse.json({ success: true, account: detailPayload });
|
|
}),
|
|
http.patch('/api/growth/:accountId/browsing', async ({ request }) => {
|
|
const body = await request.json() as Record<string, unknown>;
|
|
requests.push({ method: 'PATCH', url: request.url, body });
|
|
const currentBrowsing = detailPayload.browsing as ReturnType<typeof browsing>;
|
|
detailPayload = detail({
|
|
browsing: {
|
|
...currentBrowsing,
|
|
settings: { ...currentBrowsing.settings, ...body },
|
|
},
|
|
});
|
|
return HttpResponse.json({ success: true, account: detailPayload });
|
|
}),
|
|
http.post('/api/growth/:accountId/browsing/run', ({ request }) => {
|
|
requests.push({ method: 'POST', url: request.url, body: null });
|
|
return HttpResponse.json({ success: true, account: detailPayload });
|
|
}),
|
|
http.post('/api/growth/:accountId/pause', async ({ request }) => {
|
|
requests.push({ method: 'POST', url: request.url, body: await request.json() });
|
|
return HttpResponse.json({ success: true, account: detailPayload });
|
|
}),
|
|
http.post('/api/growth/:accountId/resume', ({ request }) => {
|
|
requests.push({ method: 'POST', url: request.url, body: null });
|
|
return HttpResponse.json({ success: true, account: detailPayload });
|
|
}),
|
|
http.post('/api/growth/:accountId/subreddits', async ({ request }) => {
|
|
requests.push({ method: 'POST', url: request.url, body: await request.json() });
|
|
return HttpResponse.json({ success: true, account: detailPayload });
|
|
}),
|
|
http.patch('/api/growth/:accountId/subreddits/:subreddit', async ({ request }) => {
|
|
requests.push({ method: 'PATCH', url: request.url, body: await request.json() });
|
|
return HttpResponse.json({ success: true, account: detailPayload });
|
|
}),
|
|
http.delete('/api/growth/:accountId/subreddits/:subreddit', ({ request }) => {
|
|
requests.push({ method: 'DELETE', url: request.url, body: null });
|
|
return HttpResponse.json({ success: true, account: detailPayload });
|
|
}),
|
|
);
|
|
|
|
function renderGrowth() {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
|
});
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<GrowthPage />
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
|
afterEach(() => {
|
|
server.resetHandlers();
|
|
cleanup();
|
|
});
|
|
afterAll(() => server.close());
|
|
|
|
beforeEach(() => {
|
|
overviewPayload = [summary()];
|
|
detailPayload = detail();
|
|
requests.length = 0;
|
|
});
|
|
|
|
describe('GrowthPage', () => {
|
|
it('says what the feature does, including that comments are unreviewed', async () => {
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByRole('heading', { level: 1, name: 'Account growth' })).toBeVisible();
|
|
expect(screen.getByText(/posted without review/i)).toBeVisible();
|
|
});
|
|
|
|
it('shows browsing sessions and can enable automatic upvotes', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
expect(await screen.findByRole('heading', { name: 'Browsing sessions' })).toBeVisible();
|
|
expect(screen.getByText('No Reddit votes will be submitted.')).toBeVisible();
|
|
expect(screen.getByText('0 / 4')).toBeVisible();
|
|
await user.click(screen.getByLabelText('Run browsing sessions'));
|
|
|
|
await waitFor(() => expect(requests).toContainEqual(expect.objectContaining({
|
|
method: 'PATCH', body: { enabled: true },
|
|
})));
|
|
await waitFor(() => expect(screen.getByRole('button', { name: 'Run session now' })).toBeEnabled());
|
|
expect(requests[0].url).toContain('/api/growth/%40t2_grower%3Areddit.com/browsing');
|
|
await user.click(screen.getByLabelText('Submit selected upvotes'));
|
|
await waitFor(() => expect(requests).toContainEqual(expect.objectContaining({
|
|
method: 'PATCH', body: { autoVoteEnabled: true },
|
|
})));
|
|
});
|
|
|
|
it('runs an enabled browsing session immediately', async () => {
|
|
const currentBrowsing = browsing(true);
|
|
detailPayload = detail({
|
|
browsing: {
|
|
...currentBrowsing,
|
|
settings: { ...currentBrowsing.settings, enabled: true },
|
|
},
|
|
});
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
await user.click(await screen.findByRole('button', { name: 'Run session now' }));
|
|
|
|
await waitFor(() => expect(requests).toContainEqual(expect.objectContaining({
|
|
method: 'POST', body: null,
|
|
})));
|
|
});
|
|
|
|
it('updates browsing session pacing and size', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
const gap = await screen.findByLabelText('Minimum session gap');
|
|
await user.clear(gap);
|
|
await user.type(gap, '90');
|
|
await user.click(screen.getByRole('button', { name: 'Save session gap' }));
|
|
|
|
const size = screen.getByLabelText('Selections per session');
|
|
await user.clear(size);
|
|
await user.type(size, '4');
|
|
await user.click(screen.getByRole('button', { name: 'Save session size' }));
|
|
|
|
await waitFor(() => expect(requests).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ method: 'PATCH', body: { minimumGapMs: 5_400_000 } }),
|
|
expect.objectContaining({ method: 'PATCH', body: { selectionsPerSession: 4 } }),
|
|
])));
|
|
});
|
|
|
|
it('updates the browsing daily target range together', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
const minimum = await screen.findByLabelText('Daily minimum');
|
|
await user.clear(minimum);
|
|
await user.type(minimum, '6');
|
|
const maximum = screen.getByLabelText('Daily maximum');
|
|
await user.clear(maximum);
|
|
await user.type(maximum, '9');
|
|
await user.click(screen.getByRole('button', { name: 'Save daily range' }));
|
|
|
|
await waitFor(() => expect(requests).toContainEqual(expect.objectContaining({
|
|
method: 'PATCH', body: { dailyTargetMin: 6, dailyTargetMax: 9 },
|
|
})));
|
|
});
|
|
|
|
it('rejects an inverted browsing daily target range in the dashboard', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
const minimum = await screen.findByLabelText('Daily minimum');
|
|
await user.clear(minimum);
|
|
await user.type(minimum, '8');
|
|
const maximum = screen.getByLabelText('Daily maximum');
|
|
await user.clear(maximum);
|
|
await user.type(maximum, '4');
|
|
|
|
expect(screen.getByRole('button', { name: 'Save daily range' })).toBeDisabled();
|
|
expect(screen.getAllByText(/minimum no greater than the maximum/i)).toHaveLength(2);
|
|
});
|
|
|
|
it('shows an empty state when there are no accounts', async () => {
|
|
overviewPayload = [];
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('No Reddit accounts yet')).toBeVisible();
|
|
});
|
|
|
|
it('surfaces the allowance, trust level and comment health', async () => {
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('1 / 3')).toBeVisible();
|
|
expect(screen.getByText('Used in last 24h')).toBeVisible();
|
|
expect(screen.getByText('1 of 4 → 3/day earned')).toBeVisible();
|
|
// 6 posted less 1 removed.
|
|
expect(screen.getByText('5')).toBeVisible();
|
|
expect(screen.getByText('4')).toBeVisible();
|
|
});
|
|
|
|
it('explains where the allowance comes from and that it is a rolling window', async () => {
|
|
renderGrowth();
|
|
|
|
// The number is derived from the trust level, not typed in by the operator,
|
|
// which is the thing most likely to confuse someone reading the page.
|
|
expect(await screen.findByText(/level 0 permits 2 comments a day/i)).toBeVisible();
|
|
expect(screen.getByText(/rolling 24-hour window rather than a midnight reset/i)).toBeVisible();
|
|
expect(screen.getByText(/Two removals in a row/i)).toBeVisible();
|
|
});
|
|
|
|
describe('daily limit', () => {
|
|
it('shows the allowance as automatic until an operator overrides it', async () => {
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('Automatic — 3/day')).toBeVisible();
|
|
expect(screen.getByText('1 of 4 → 3/day earned')).toBeVisible();
|
|
});
|
|
|
|
it('sets a limit through the API', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
const field = await screen.findByLabelText('Daily limit');
|
|
await user.clear(field);
|
|
await user.type(field, '9');
|
|
await user.click(screen.getByRole('button', { name: 'Save limit' }));
|
|
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0]).toMatchObject({ method: 'PATCH', body: { dailyLimit: 9 } });
|
|
});
|
|
|
|
it('clearing the field hands the allowance back to the ladder', async () => {
|
|
detailPayload = detail({ settings: settings({ dailyLimit: 9, dailyAllowance: 9 }) });
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
await user.clear(await screen.findByLabelText('Daily limit'));
|
|
await user.click(screen.getByRole('button', { name: 'Save limit' }));
|
|
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0]).toMatchObject({ body: { dailyLimit: null } });
|
|
});
|
|
|
|
it('does not submit an out-of-range limit', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
const field = await screen.findByLabelText('Daily limit');
|
|
await user.clear(field);
|
|
await user.type(field, '99');
|
|
await user.click(screen.getByRole('button', { name: 'Save limit' }));
|
|
|
|
expect(requests).toHaveLength(0);
|
|
});
|
|
|
|
it('shows an override and warns when it exceeds what was earned', async () => {
|
|
detailPayload = detail({
|
|
settings: settings({ trustLevel: 0, dailyLimit: 12, dailyAllowance: 12, adaptiveAllowance: 2 }),
|
|
});
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('Set to 12/day')).toBeVisible();
|
|
expect(screen.getByText(/above the 2\/day this account has earned/i)).toBeVisible();
|
|
// The safety stop is not something a limit can switch off.
|
|
expect(screen.getByText(/pause on two removals in a row.*still applies/i)).toBeVisible();
|
|
});
|
|
|
|
it('does not warn when the override sits within what was earned', async () => {
|
|
detailPayload = detail({
|
|
settings: settings({ trustLevel: 4, dailyLimit: 3, dailyAllowance: 3, adaptiveAllowance: 12 }),
|
|
});
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('Set to 3/day')).toBeVisible();
|
|
expect(screen.queryByText(/this account has earned/i)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('warns when a community cap exceeds what the account allowance permits', async () => {
|
|
detailPayload = detail({
|
|
settings: settings({ trustLevel: 0, dailyAllowance: 2 }),
|
|
subreddits: [{
|
|
subreddit: 'AskUK',
|
|
enabled: true,
|
|
weight: 1,
|
|
dailyCap: 10,
|
|
disabledReason: null,
|
|
cooldownUntil: null,
|
|
addedAt: 1,
|
|
postedLast24h: 1,
|
|
}],
|
|
});
|
|
renderGrowth();
|
|
|
|
// A cap of 10 with an allowance of 2 reads as "10 available" otherwise.
|
|
expect(await screen.findByText(/Only 2 of these can actually be used today/i)).toBeVisible();
|
|
});
|
|
|
|
it('does not warn when the cap sits within the allowance', async () => {
|
|
detailPayload = detail({
|
|
settings: settings({ dailyAllowance: 5 }),
|
|
subreddits: [{
|
|
subreddit: 'AskUK',
|
|
enabled: true,
|
|
weight: 1,
|
|
dailyCap: 2,
|
|
disabledReason: null,
|
|
cooldownUntil: null,
|
|
addedAt: 1,
|
|
postedLast24h: 0,
|
|
}],
|
|
});
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('r/AskUK')).toBeVisible();
|
|
expect(screen.queryByText(/can actually be used today/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('toggles growth off through the API', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
await user.click(await screen.findByLabelText('Grow this account'));
|
|
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0]).toMatchObject({ method: 'PATCH', body: { enabled: false } });
|
|
});
|
|
|
|
it('encodes the Matrix-shaped account ID in every request path', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
await user.click(await screen.findByLabelText('Grow this account'));
|
|
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0].url).toContain(encodeURIComponent(ACCOUNT_ID));
|
|
expect(requests[0].url).not.toContain('@t2_grower:reddit.com');
|
|
});
|
|
|
|
it('adds a community', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
await user.type(await screen.findByLabelText('Add a community'), 'CasualUK');
|
|
await user.click(screen.getByRole('button', { name: 'Add community' }));
|
|
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0]).toMatchObject({ method: 'POST', body: { subreddit: 'CasualUK' } });
|
|
});
|
|
|
|
it('does not submit an empty community name', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
await user.click(await screen.findByRole('button', { name: 'Add community' }));
|
|
|
|
expect(requests).toHaveLength(0);
|
|
});
|
|
|
|
it('toggles and removes a community', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
const rows = await screen.findAllByText(/^r\//);
|
|
const askUk = rows[0].closest('.growth-subreddit-row') as HTMLElement;
|
|
await user.click(within(askUk).getByRole('button', { name: 'Disable' }));
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0]).toMatchObject({ method: 'PATCH', body: { enabled: false } });
|
|
|
|
await user.click(within(askUk).getByRole('button', { name: 'Remove' }));
|
|
await waitFor(() => expect(requests).toHaveLength(2));
|
|
expect(requests[1].method).toBe('DELETE');
|
|
});
|
|
|
|
it('shows why Reddit refused a community', async () => {
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText(/Reddit refused this account here: banned \(r\/cats\)/))
|
|
.toBeVisible();
|
|
});
|
|
|
|
it('saves a changed daily cap on blur, and not before', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
const capInput = await screen.findByLabelText('Daily cap for r/AskUK');
|
|
await user.clear(capInput);
|
|
await user.type(capInput, '4');
|
|
expect(requests).toHaveLength(0);
|
|
|
|
await user.tab();
|
|
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0]).toMatchObject({ method: 'PATCH', body: { dailyCap: 4 } });
|
|
});
|
|
|
|
it('shows the posted comment text as the audit record', async () => {
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('the train is usually quicker than the coach at that time of day'))
|
|
.toBeVisible();
|
|
expect(screen.getByText('7 points')).toBeVisible();
|
|
expect(screen.getByRole('link', { name: 'View on Reddit' }))
|
|
.toHaveAttribute('href', 'https://www.reddit.com/r/AskUK/comments/p1/_/c1/');
|
|
});
|
|
|
|
it('shows a discarded comment and the reason it was refused', async () => {
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('Discarded')).toBeVisible();
|
|
expect(screen.getByText('Comment solicits contact or promotes a profile')).toBeVisible();
|
|
expect(screen.getByText('check out my profile for more')).toBeVisible();
|
|
});
|
|
|
|
describe('when the account is paused', () => {
|
|
beforeEach(() => {
|
|
const paused = settings({
|
|
pausedReason: 'A posted comment is not visible to signed-out visitors',
|
|
pausedAt: 1_700_000_000_000,
|
|
});
|
|
overviewPayload = [summary({ settings: paused })];
|
|
detailPayload = detail({ settings: paused });
|
|
});
|
|
|
|
it('warns prominently and tells the operator to review before resuming', async () => {
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('Growth is paused for this account.')).toBeVisible();
|
|
expect(screen.getByText(/not visible to signed-out visitors/)).toBeVisible();
|
|
expect(screen.getByText(/needs a rest, not another attempt/i)).toBeVisible();
|
|
expect(screen.getByText(/Growth is paused for u\/grower\./)).toBeVisible();
|
|
});
|
|
|
|
it('offers resume instead of pause, and only an operator can clear it', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
const resume = await screen.findByRole('button', { name: 'Resume growth' });
|
|
expect(screen.queryByRole('button', { name: 'Pause growth' })).not.toBeInTheDocument();
|
|
|
|
await user.click(resume);
|
|
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0].url).toContain('/resume');
|
|
});
|
|
});
|
|
|
|
it('pauses on demand', async () => {
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
await user.click(await screen.findByRole('button', { name: 'Pause growth' }));
|
|
|
|
await waitFor(() => expect(requests).toHaveLength(1));
|
|
expect(requests[0].url).toContain('/pause');
|
|
});
|
|
|
|
it('reports a failed load', async () => {
|
|
server.use(http.get('/api/growth', () => HttpResponse.json({ error: 'nope' }, { status: 500 })));
|
|
renderGrowth();
|
|
|
|
expect(await screen.findByText('Could not load account growth')).toBeVisible();
|
|
});
|
|
|
|
it('surfaces a rejected settings change', async () => {
|
|
server.use(http.patch('/api/growth/:accountId', () => HttpResponse.json(
|
|
{ error: 'Invalid growth settings' },
|
|
{ status: 400 },
|
|
)));
|
|
renderGrowth();
|
|
const user = userEvent.setup();
|
|
|
|
await user.click(await screen.findByLabelText('Grow this account'));
|
|
|
|
expect(await screen.findByText('Invalid growth settings')).toBeVisible();
|
|
});
|
|
});
|