chore (ci): re-add user event tests (#3288)
### **PR Type** Tests, Enhancement ___ ### **Description** - Reintroduce user event tests in CI - Update SvelteKit example tests to e2e suite - Refactor TestUserEvent class for improved testing - Add new tests for database and backup features ___ ### **Changes walkthrough** 📝 <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><details><summary>7 files</summary><table> <tr> <td><strong>DateTimePicker.test.tsx</strong><dd><code>Add comprehensive tests for DateTimePicker component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-c7076012eb33d6f60049710638b5ad19c2f310b8c250c79f1905be7e0a30b00a">+177/-0</a> </td> </tr> <tr> <td><strong>TimePicker.test.tsx</strong><dd><code>Update TimePicker tests to use TestUserEvent</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-784f69003ebbc9e39837b920007cef14125a5fc48bb9114226820bcb2b0827b0">+6/-7</a> </td> </tr> <tr> <td><strong>TransferProjectDialog.test.tsx</strong><dd><code>Add tests for TransferProjectDialog component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-d4ebdb8af76a7c9e73606708718c3448445545259ad553d73b6d322408e3eb8c">+234/-0</a> </td> </tr> <tr> <td><strong>ImportBackupTabContent.test.tsx</strong><dd><code>Add tests for ImportBackupTabContent component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-753e5e6735a2d612b6ccc6617c053017ba591a763182fa28a8fc302731c3f347">+267/-0</a> </td> </tr> <tr> <td><strong>DatabasePiTRSettings.test.tsx</strong><dd><code>Add tests for DatabasePiTRSettings component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-85d1f82a571b56469eab40dcc164fdd1e107fba79611ddd5cca7c191fe5117b4">+188/-0</a> </td> </tr> <tr> <td><strong>ResourcesForm.test.tsx</strong><dd><code>Update ResourcesForm tests to use TestUserEvent</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-8828db70c080be6fc19f88059b08587584f1c23c9159092d6b186ca82a1943aa">+60/-55</a> </td> </tr> <tr> <td><strong>resourceSettingsQuery.ts</strong><dd><code>Update resourcesAvailableQuery mock data</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-49b3a2a24ead48f97ace0b90f1ecaf4d4edbdef17109e29f5101016515e5946a">+12/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>2 files</summary><table> <tr> <td><strong>testUtils.tsx</strong><dd><code>Refactor TestUserEvent class and remove utility functions</code></dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-78f29250407edf853a353b48242d3cee59aa5724f38a60bb23bebdfc1ea2f9b5">+35/-18</a> </td> </tr> <tr> <td><strong>package.json</strong><dd><code>Update SvelteKit example test script to e2e</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a> </td> </tr> </table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>1 files</summary><table> <tr> <td><strong>nasty-cherries-cover.md</strong><dd><code>Add changeset for user event CI tests</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-653e520d91e00e8c62155076a5acfb2a606381f63c4c87b42ac70d23e7c97a01">+6/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>1 files</summary><table> <tr> <td><strong>PointInTimeBackupInfo.test.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3288/files#diff-3980415ca79bf039abb469281fff9b1dc1de0a1ef52b4044d8c6f529538b6edf">+356/-0</a> </td> </tr> </table></details></td></tr></tr></tbody></table> ___ > <details> <summary> Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about PR-Agent usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details>
This commit is contained in:
6
.changeset/nasty-cherries-cover.md
Normal file
6
.changeset/nasty-cherries-cover.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@nhost-examples/sveltekit': minor
|
||||
'@nhost/dashboard': minor
|
||||
---
|
||||
|
||||
chore: re-add user event ci tests, updated sveltekit example tests to e2e suite
|
||||
@@ -0,0 +1,177 @@
|
||||
import { isTZDate } from '@/components/common/TimePicker/time-picker-utils';
|
||||
import { render, screen, TestUserEvent, waitFor } from '@/tests/testUtils';
|
||||
import { isBefore, startOfDay } from 'date-fns-v4';
|
||||
import { useState } from 'react';
|
||||
import { TZDate } from 'react-day-picker';
|
||||
import { vi } from 'vitest';
|
||||
import DateTimePicker, { type DateTimePickerProps } from './DateTimePicker';
|
||||
|
||||
vi.mock('@/utils/timezoneUtils', async () => {
|
||||
const actualTimezoneUtils = await vi.importActual<any>(
|
||||
'@/utils/timezoneUtils',
|
||||
);
|
||||
return {
|
||||
...actualTimezoneUtils,
|
||||
guessTimezone: () => 'Europe/Helsinki',
|
||||
};
|
||||
});
|
||||
|
||||
const earliestBackupDate = '2025-03-13T02:00:05.000Z';
|
||||
|
||||
function TestComponent(
|
||||
props: Omit<DateTimePickerProps, 'dateTime' | 'onDateTimeChange'>,
|
||||
) {
|
||||
const [dateTime, setDateTime] = useState(earliestBackupDate);
|
||||
|
||||
function isCalendarDayDisabled(date: Date | TZDate) {
|
||||
if (isTZDate(date)) {
|
||||
const utcDay = new Date(date.getTime()).toISOString();
|
||||
const tzDate = new TZDate(utcDay, date.timeZone);
|
||||
const earliestBackupDateInTz = new TZDate(
|
||||
earliestBackupDate,
|
||||
date.timeZone,
|
||||
);
|
||||
return isBefore(startOfDay(tzDate), startOfDay(earliestBackupDateInTz));
|
||||
}
|
||||
|
||||
return isBefore(
|
||||
startOfDay(new Date(date.getTime()).toISOString()),
|
||||
startOfDay(earliestBackupDate),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 data-testid="utcDate">{dateTime}</h1>
|
||||
<DateTimePicker
|
||||
{...props}
|
||||
isCalendarDayDisabled={isCalendarDayDisabled}
|
||||
dateTime={dateTime}
|
||||
onDateTimeChange={setDateTime}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
describe('DateTimePicker', () => {
|
||||
test('when the date changes datetime is emitted in utc string format', async () => {
|
||||
render(<TestComponent />);
|
||||
const user = new TestUserEvent();
|
||||
|
||||
await user.click(await screen.findByTestId('dateTimePickerTrigger'));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: 'Select' }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(await screen.getByText('March 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Go to the Next Month' }),
|
||||
);
|
||||
expect(screen.getByText('April 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(await screen.getByText('13'));
|
||||
|
||||
const hoursInput = await screen.getByLabelText('Hours');
|
||||
await user.type(hoursInput, '11');
|
||||
|
||||
const minutesInput = await screen.getByLabelText('Minutes');
|
||||
await user.type(minutesInput, '12');
|
||||
|
||||
const secondsInput = await screen.getByLabelText('Seconds');
|
||||
await user.type(secondsInput, '13');
|
||||
|
||||
user.click(await screen.getByRole('button', { name: 'Select' }));
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.queryByRole('button', { name: 'Select' }),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('utcDate')).toHaveTextContent(
|
||||
'2025-04-13T08:12:13.000Z',
|
||||
);
|
||||
});
|
||||
|
||||
test('timezone can be changed and the calendar is updated', async () => {
|
||||
await waitFor(() => render(<TestComponent withTimezone />));
|
||||
const user = new TestUserEvent();
|
||||
|
||||
await user.click(await screen.findByTestId('dateTimePickerTrigger'));
|
||||
|
||||
expect(await screen.findByText(/Timezone:/)).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('timezoneSettingsButton'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
|
||||
'Timezone: UTC+02:00',
|
||||
);
|
||||
expect(await screen.getByText('12')).toBeDisabled();
|
||||
|
||||
await user.click(await screen.findByTestId('timezoneSettingsButton'));
|
||||
const tzInput = await screen.findByPlaceholderText('Search timezones...');
|
||||
expect(tzInput).toBeInTheDocument();
|
||||
|
||||
await user.type(tzInput, 'America/Chicago{ArrowDown}{Enter}');
|
||||
|
||||
expect(
|
||||
await screen.queryByPlaceholderText('Search timezones...'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
|
||||
'Timezone: UTC-05:00',
|
||||
);
|
||||
|
||||
const selectedDay = screen.getByText('12');
|
||||
expect(selectedDay).not.toBeDisabled();
|
||||
expect(await screen.getByText('11')).toBeDisabled();
|
||||
const gridCell = selectedDay.closest('[role="gridcell"]');
|
||||
expect(gridCell).toHaveClass('[&>button]:bg-primary');
|
||||
});
|
||||
|
||||
test('Displays the correct time zone offset when changing the selected date from standard time (ST) to daylight saving time (DST)', async () => {
|
||||
await waitFor(() => render(<TestComponent withTimezone />));
|
||||
const user = new TestUserEvent();
|
||||
|
||||
await user.click(await screen.findByTestId('dateTimePickerTrigger'));
|
||||
|
||||
expect(await screen.findByText(/Timezone:/)).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('timezoneSettingsButton'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
|
||||
'Timezone: UTC+02:00',
|
||||
);
|
||||
|
||||
expect(await screen.getByText('March 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Go to the Next Month' }),
|
||||
);
|
||||
|
||||
expect(screen.getByText('April 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(await screen.getByText('18'));
|
||||
|
||||
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
|
||||
'Timezone: UTC+03:00',
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Go to the Previous Month' }),
|
||||
);
|
||||
|
||||
expect(await screen.getByText('March 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(await screen.getByText('21'));
|
||||
|
||||
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
|
||||
'Timezone: UTC+02:00',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import { render, screen, TestUserEvent } from '@/tests/testUtils';
|
||||
import { guessTimezone } from '@/utils/timezoneUtils';
|
||||
import { TZDate } from '@date-fns/tz';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { format } from 'date-fns-v4';
|
||||
import { useState } from 'react';
|
||||
@@ -36,7 +35,7 @@ describe('TimePicker', () => {
|
||||
expect(await screen.getByText(/Time:/i)).toHaveTextContent(
|
||||
'Time: 03:00:05',
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const user = new TestUserEvent();
|
||||
const hoursInput = await screen.getByLabelText('Hours');
|
||||
await user.type(hoursInput, '18');
|
||||
expect(await screen.getByText(/Time:/i)).toHaveTextContent(
|
||||
@@ -46,7 +45,7 @@ describe('TimePicker', () => {
|
||||
|
||||
test('only valid hours(0-23), minutes(0-59) and seconds(0-59) are allowed', async () => {
|
||||
render(<TestComponent dateTime="2025-03-10T03:00:05" />);
|
||||
const user = userEvent.setup();
|
||||
const user = new TestUserEvent();
|
||||
const hoursInput = await screen.getByLabelText('Hours');
|
||||
await user.type(hoursInput, '30');
|
||||
expect(await screen.getByText(/Time:/i)).toHaveTextContent(
|
||||
@@ -61,7 +60,7 @@ describe('TimePicker', () => {
|
||||
|
||||
test('Updates only the minutes of the date object', async () => {
|
||||
render(<TestComponent dateTime="2025-03-10T03:00:05" />);
|
||||
const user = userEvent.setup();
|
||||
const user = new TestUserEvent();
|
||||
const minutesInput = await screen.getByLabelText('Minutes');
|
||||
await user.type(minutesInput, '44');
|
||||
expect(await screen.getByText(/Time:/i)).toHaveTextContent(
|
||||
@@ -71,7 +70,7 @@ describe('TimePicker', () => {
|
||||
|
||||
test('Updates only the seconds of the date object', async () => {
|
||||
render(<TestComponent dateTime="2025-03-10T03:00:05" />);
|
||||
const user = userEvent.setup();
|
||||
const user = new TestUserEvent();
|
||||
const secondsInput = await screen.getByLabelText('Seconds');
|
||||
await user.type(secondsInput, '11');
|
||||
expect(await screen.getByText(/Time:/i)).toHaveTextContent(
|
||||
@@ -84,7 +83,7 @@ describe('TimePicker', () => {
|
||||
expect(await screen.getByText(/Date class:/i)).toHaveTextContent(
|
||||
'Date class: TZDate',
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const user = new TestUserEvent();
|
||||
|
||||
const hoursInput = await screen.getByLabelText('Hours');
|
||||
await user.type(hoursInput, '18');
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import {
|
||||
mockMatchMediaValue,
|
||||
mockOrganization,
|
||||
mockOrganizations,
|
||||
mockOrganizationsWithNewOrg,
|
||||
newOrg,
|
||||
} from '@/tests/mocks';
|
||||
import { getOrganization } from '@/tests/msw/mocks/graphql/getOrganizationQuery';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import { prefetchNewAppQuery } from '@/tests/msw/mocks/graphql/prefetchNewAppQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import {
|
||||
createGraphqlMockResolver,
|
||||
fireEvent,
|
||||
mockPointerEvent,
|
||||
queryClient,
|
||||
render,
|
||||
screen,
|
||||
TestUserEvent,
|
||||
waitFor,
|
||||
} from '@/tests/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { useState } from 'react';
|
||||
import { afterAll, beforeAll, vi } from 'vitest';
|
||||
import TransferProjectDialog from './TransferProjectDialog';
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(mockMatchMediaValue),
|
||||
});
|
||||
|
||||
mockPointerEvent();
|
||||
|
||||
const getUseRouterObject = (session_id?: string) => ({
|
||||
basePath: '',
|
||||
pathname: '/orgs/xyz/projects/test-project',
|
||||
route: '/orgs/[orgSlug]/projects/[appSubdomain]',
|
||||
asPath: '/orgs/xyz/projects/test-project',
|
||||
isLocaleDomain: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
query: {
|
||||
orgSlug: 'xyz',
|
||||
appSubdomain: 'test-project',
|
||||
session_id,
|
||||
},
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
reload: vi.fn(),
|
||||
back: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
beforePopState: vi.fn(),
|
||||
events: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
},
|
||||
isFallback: false,
|
||||
});
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useRouter: vi.fn(),
|
||||
useOrgs: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useOrgs', async () => {
|
||||
const actualUseOrgs = await vi.importActual<any>(
|
||||
'@/features/orgs/projects/hooks/useOrgs',
|
||||
);
|
||||
return {
|
||||
...actualUseOrgs,
|
||||
useOrgs: mocks.useOrgs,
|
||||
};
|
||||
});
|
||||
|
||||
const postOrganizationRequestResolver = createGraphqlMockResolver(
|
||||
'postOrganizationRequest',
|
||||
'mutation',
|
||||
);
|
||||
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: mocks.useRouter,
|
||||
}));
|
||||
|
||||
export function DialogWrapper({
|
||||
defaultOpen = true,
|
||||
}: {
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
return <TransferProjectDialog open={open} setOpen={setOpen} />;
|
||||
}
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear();
|
||||
mocks.useRouter.mockRestore();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('opens create org dialog when selecting "create new org" and closes transfer dialog', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
const user = new TestUserEvent();
|
||||
|
||||
server.use(getProjectQuery);
|
||||
server.use(getOrganization);
|
||||
mocks.useOrgs.mockImplementation(() => ({
|
||||
orgs: mockOrganizations,
|
||||
currentOrg: mockOrganization,
|
||||
loading: false,
|
||||
refetch: vi.fn(),
|
||||
}));
|
||||
server.use(prefetchNewAppQuery);
|
||||
|
||||
render(<DialogWrapper />);
|
||||
const organizationCombobox = await screen.findByRole('combobox', {
|
||||
name: /Organization/i,
|
||||
});
|
||||
|
||||
expect(organizationCombobox).toBeInTheDocument();
|
||||
|
||||
await user.click(organizationCombobox);
|
||||
|
||||
const newOrgOption = await screen.findByRole('option', {
|
||||
name: 'New Organization',
|
||||
});
|
||||
await user.click(newOrgOption);
|
||||
expect(organizationCombobox).toHaveTextContent('New Organization');
|
||||
|
||||
const submitButton = await screen.findByText('Continue');
|
||||
expect(submitButton).toHaveTextContent('Continue');
|
||||
|
||||
fireEvent(
|
||||
submitButton,
|
||||
new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const newOrgTitle = await screen.findByText('New Organization');
|
||||
expect(newOrgTitle).toBeInTheDocument();
|
||||
const closeButton = await screen.findByText('Close');
|
||||
fireEvent(
|
||||
closeButton,
|
||||
new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(newOrgTitle).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const submitButtonAfterClosingNewOrgDialog =
|
||||
await screen.findByText('Continue');
|
||||
await waitFor(() => {
|
||||
expect(submitButtonAfterClosingNewOrgDialog).toHaveTextContent('Continue');
|
||||
});
|
||||
});
|
||||
|
||||
test(`transfer dialog opens automatically when there is a session_id and selects the ${newOrg.name} from the dropdown`, async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject('session_id'));
|
||||
server.use(getProjectQuery);
|
||||
server.use(getOrganization);
|
||||
mocks.useOrgs.mockImplementation(() => ({
|
||||
orgs: mockOrganizations,
|
||||
currentOrg: mockOrganization,
|
||||
loading: false,
|
||||
refetch: vi.fn(),
|
||||
}));
|
||||
server.use(prefetchNewAppQuery);
|
||||
server.use(postOrganizationRequestResolver.handler);
|
||||
|
||||
render(<DialogWrapper defaultOpen={false} />);
|
||||
const processingNewOrgText = await screen.findByText(
|
||||
'Processing new organization request',
|
||||
);
|
||||
|
||||
expect(processingNewOrgText).toBeInTheDocument();
|
||||
|
||||
const closeButton = await screen.findByText('Close');
|
||||
|
||||
fireEvent(
|
||||
closeButton,
|
||||
new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {});
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
|
||||
postOrganizationRequestResolver.resolve({
|
||||
billingPostOrganizationRequest: {
|
||||
Status: 'COMPLETED',
|
||||
Slug: newOrg.slug,
|
||||
ClientSecret: null,
|
||||
__typename: 'PostOrganizationRequestResponse',
|
||||
},
|
||||
});
|
||||
|
||||
mocks.useOrgs.mockImplementation(() => ({
|
||||
orgs: mockOrganizationsWithNewOrg,
|
||||
currentOrg: mockOrganization,
|
||||
loading: false,
|
||||
refetch: vi.fn(),
|
||||
}));
|
||||
|
||||
const organizationCombobox = await screen.findByRole('combobox', {
|
||||
name: /Organization/i,
|
||||
});
|
||||
|
||||
expect(organizationCombobox).toHaveTextContent(newOrg.name);
|
||||
|
||||
const submitButton = await screen.findByText('Transfer');
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
import {
|
||||
fetchPiTRBaseBackups,
|
||||
mockApplication,
|
||||
mockMatchMediaValue,
|
||||
} from '@/tests/mocks';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import {
|
||||
mockPointerEvent,
|
||||
render,
|
||||
screen,
|
||||
TestUserEvent,
|
||||
waitFor,
|
||||
} from '@/tests/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { Tabs } from '@/components/ui/v3/tabs';
|
||||
import { getOrganization } from '@/tests/msw/mocks/graphql/getOrganizationQuery';
|
||||
import {
|
||||
getPiTRNotEnabledPostgresSettings,
|
||||
getPostgresSettings,
|
||||
} from '@/tests/msw/mocks/graphql/getPostgresSettings';
|
||||
import {
|
||||
getEmptyProjectsQuery,
|
||||
getProjectsQuery,
|
||||
} from '@/tests/msw/mocks/graphql/getProjectsQuery';
|
||||
import ImportBackupContent from './ImportBackupTabContent';
|
||||
|
||||
function TestComponent() {
|
||||
return (
|
||||
<Tabs value="importBackup">
|
||||
<ImportBackupContent />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
mockPointerEvent();
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(mockMatchMediaValue),
|
||||
});
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useGetPiTrBaseBackupsLazyQuery: vi.fn(),
|
||||
fetchPiTRBaseBackups: vi.fn(),
|
||||
restoreApplicationDatabase: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/__generated__/graphql', async () => {
|
||||
const actual = await vi.importActual<any>('@/utils/__generated__/graphql');
|
||||
return {
|
||||
...actual,
|
||||
useGetPiTrBaseBackupsLazyQuery: mocks.useGetPiTrBaseBackupsLazyQuery,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/utils/timezoneUtils', async () => {
|
||||
const actualTimezoneUtils = await vi.importActual<any>(
|
||||
'@/utils/timezoneUtils',
|
||||
);
|
||||
return {
|
||||
...actualTimezoneUtils,
|
||||
guessTimezone: () => 'Europe/Helsinki',
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/features/orgs/hooks/useRestoreApplicationDatabasePiTR', () => ({
|
||||
useRestoreApplicationDatabasePiTR: () => ({
|
||||
restoreApplicationDatabase: mocks.restoreApplicationDatabase,
|
||||
loading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useProject', async () => ({
|
||||
useProject: () => ({ project: mockApplication }),
|
||||
}));
|
||||
|
||||
describe('ImportBackupContent', () => {
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("will display the target project's name and a select with the projects from the same region", async () => {
|
||||
const user = new TestUserEvent();
|
||||
server.use(getOrganization);
|
||||
server.use(getProjectsQuery);
|
||||
|
||||
render(<TestComponent />);
|
||||
expect(
|
||||
await screen.getByText(
|
||||
`${mockApplication.name} (${mockApplication.region.name})`,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const projectComboBox = await screen.findByRole('combobox');
|
||||
|
||||
await user.click(projectComboBox);
|
||||
// check for only projects from the same region are listed
|
||||
expect(screen.getByRole('option', { name: /pitr14/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('option', { name: /pitr-not-enabled/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('option', { name: /pitr-test/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('option', { name: /pitr-region-test-eu/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('that warning is displayed if there are no other projects in the same organization', async () => {
|
||||
server.use(getOrganization);
|
||||
server.use(getEmptyProjectsQuery);
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
/There are no other projects within the region:/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('will schedule an import from the selected project', async () => {
|
||||
const user = new TestUserEvent();
|
||||
server.use(getOrganization);
|
||||
server.use(getProjectsQuery);
|
||||
server.use(getPostgresSettings);
|
||||
mocks.useGetPiTrBaseBackupsLazyQuery.mockImplementation(() => [
|
||||
fetchPiTRBaseBackups,
|
||||
{ loading: false },
|
||||
]);
|
||||
|
||||
render(<TestComponent />);
|
||||
expect(
|
||||
await screen.getByText(
|
||||
`${mockApplication.name} (${mockApplication.region.name})`,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const projectComboBox = await screen.findByRole('combobox');
|
||||
|
||||
await user.click(projectComboBox);
|
||||
|
||||
await waitFor(async () => {
|
||||
await user.click(
|
||||
screen.getByRole('option', {
|
||||
name: 'pitr14 (us-east-1)',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.getByText('Import backup from pitr14 (us-east-1)'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const startImportButton = await screen.getByRole('button', {
|
||||
name: 'Start import',
|
||||
});
|
||||
await user.click(startImportButton);
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.getByRole('button', { name: 'Import backup' }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
const dateTimePickerButton = await screen.getByRole('button', {
|
||||
name: /UTC/i,
|
||||
});
|
||||
|
||||
await user.click(dateTimePickerButton);
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.getByRole('button', { name: 'Select' }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
await user.click(await screen.getByText('13'));
|
||||
|
||||
const hoursInput = await screen.getByLabelText('Hours');
|
||||
await waitFor(async () => {
|
||||
await user.type(hoursInput, '18');
|
||||
});
|
||||
const updatedDateTimeButton = await screen.getByRole('button', {
|
||||
name: /UTC/i,
|
||||
});
|
||||
expect(updatedDateTimeButton).toHaveTextContent(
|
||||
'13 Mar 2025, 18:00:05 (UTC+02:00)',
|
||||
);
|
||||
await user.click(await screen.getByRole('button', { name: 'Select' }));
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.queryByRole('button', { name: 'Select' }),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
|
||||
expect(updatedDateTimeButton).toHaveTextContent(
|
||||
'13 Mar 2025, 18:00:05 (UTC+02:00)',
|
||||
);
|
||||
|
||||
// check checkboxes
|
||||
|
||||
await user.click(
|
||||
await screen.getByLabelText(/I understand that restoring this backup/),
|
||||
);
|
||||
|
||||
await user.click(
|
||||
await screen.getByLabelText(/I understand this cannot be undone/),
|
||||
);
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.getByRole('button', { name: 'Import backup' }),
|
||||
).not.toBeDisabled(),
|
||||
);
|
||||
|
||||
await user.click(
|
||||
await screen.getByRole('button', { name: 'Import backup' }),
|
||||
);
|
||||
|
||||
expect(mocks.restoreApplicationDatabase.mock.calls[0][0].fromAppId).toBe(
|
||||
'pitr14-id',
|
||||
);
|
||||
|
||||
expect(
|
||||
mocks.restoreApplicationDatabase.mock.calls[0][0].recoveryTarget,
|
||||
).toBe('2025-03-13T16:00:05.000Z');
|
||||
});
|
||||
|
||||
test('Pitr is not enabled on project', async () => {
|
||||
const user = new TestUserEvent();
|
||||
server.use(getOrganization);
|
||||
server.use(getProjectsQuery);
|
||||
server.use(getPiTRNotEnabledPostgresSettings);
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
const projectComboBox = await screen.findByRole('combobox');
|
||||
|
||||
await user.click(projectComboBox);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('option', {
|
||||
name: 'pitr-not-enabled-usa (us-east-1)',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Point-in-Time Recovery is not enabled on the selected project',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,356 @@
|
||||
import {
|
||||
fetchEmptyPiTRBaseBackups,
|
||||
fetchPiTRBaseBackups,
|
||||
mockApplication,
|
||||
mockMatchMediaValue,
|
||||
} from '@/tests/mocks';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { render, screen, TestUserEvent, waitFor } from '@/tests/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { test, vi } from 'vitest';
|
||||
|
||||
import { getOrganization } from '@/tests/msw/mocks/graphql/getOrganizationQuery';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
|
||||
import PointInTimeBackupInfo from './PointInTimeBackupInfo';
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(mockMatchMediaValue),
|
||||
});
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useGetPiTrBaseBackupsLazyQuery: vi.fn(),
|
||||
fetchPiTRBaseBackups: vi.fn(),
|
||||
restoreApplicationDatabase: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/__generated__/graphql', async () => {
|
||||
const actual = await vi.importActual<any>('@/utils/__generated__/graphql');
|
||||
return {
|
||||
...actual,
|
||||
useGetPiTrBaseBackupsLazyQuery: mocks.useGetPiTrBaseBackupsLazyQuery,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/utils/timezoneUtils', async () => {
|
||||
const actualTimezoneUtils = await vi.importActual<any>(
|
||||
'@/utils/timezoneUtils',
|
||||
);
|
||||
return {
|
||||
...actualTimezoneUtils,
|
||||
guessTimezone: () => 'Europe/Helsinki',
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/features/orgs/hooks/useRestoreApplicationDatabasePiTR', () => ({
|
||||
useRestoreApplicationDatabasePiTR: () => ({
|
||||
restoreApplicationDatabase: mocks.restoreApplicationDatabase,
|
||||
loading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useProject', async () => ({
|
||||
useProject: () => ({ project: mockApplication }),
|
||||
}));
|
||||
|
||||
describe('PointInTimeBackupInfo', () => {
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
test("will display the earliest backup's date with the local timezone", async () => {
|
||||
server.use(getProjectQuery);
|
||||
server.use(getOrganization);
|
||||
mocks.useGetPiTrBaseBackupsLazyQuery.mockImplementation(() => [
|
||||
fetchPiTRBaseBackups,
|
||||
{ loading: false },
|
||||
]);
|
||||
|
||||
await waitFor(() =>
|
||||
render(<PointInTimeBackupInfo appId={mockApplication.id} />),
|
||||
);
|
||||
const earliestBackup = await screen.getByTestId('EarliestBackupDateTime');
|
||||
expect(earliestBackup).toHaveTextContent(
|
||||
'10 Mar 2025, 05:00:05 (UTC+02:00)',
|
||||
);
|
||||
});
|
||||
|
||||
test("that the system fetches the earliest backup, displays 'Project has no backups yet' message when no backups exist, and verifies that the restore button is disabled.", async () => {
|
||||
server.use(getOrganization);
|
||||
mocks.useGetPiTrBaseBackupsLazyQuery.mockImplementation(() => [
|
||||
fetchEmptyPiTRBaseBackups,
|
||||
{ loading: false },
|
||||
]);
|
||||
|
||||
await waitFor(() =>
|
||||
render(<PointInTimeBackupInfo appId={mockApplication.id} />),
|
||||
);
|
||||
|
||||
const earliestBackup = await screen.getByText(
|
||||
'Project has no backups yet.',
|
||||
);
|
||||
expect(earliestBackup).toBeInTheDocument();
|
||||
const startRestoreButton = await screen.getByRole('button', {
|
||||
name: 'Start restore',
|
||||
});
|
||||
expect(startRestoreButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('will update the date after the timezone has changed', async () => {
|
||||
server.use(getOrganization);
|
||||
mocks.useGetPiTrBaseBackupsLazyQuery.mockImplementation(() => [
|
||||
fetchPiTRBaseBackups,
|
||||
{ loading: false },
|
||||
]);
|
||||
await waitFor(() => render(<PointInTimeBackupInfo appId="randomId" />));
|
||||
const user = new TestUserEvent();
|
||||
|
||||
const earliestBackup = await screen.getByTestId('EarliestBackupDateTime');
|
||||
expect(earliestBackup).toHaveTextContent(
|
||||
'10 Mar 2025, 05:00:05 (UTC+02:00)',
|
||||
);
|
||||
|
||||
const changeTimezoneButton = await screen.getByRole('button', {
|
||||
name: 'Change timezone',
|
||||
});
|
||||
await user.click(changeTimezoneButton);
|
||||
const tzInput = await screen.getByPlaceholderText('Search timezones...');
|
||||
expect(tzInput).toBeInTheDocument();
|
||||
await user.type(tzInput, 'Asia/Amman{ArrowDown}{Enter}');
|
||||
await waitFor(() => expect(tzInput).not.toBeInTheDocument());
|
||||
const updatedEarliestBackup = await screen.getByTestId(
|
||||
'EarliestBackupDateTime',
|
||||
);
|
||||
expect(updatedEarliestBackup).toHaveTextContent(
|
||||
'10 Mar 2025, 06:00:05 (UTC+03:00)',
|
||||
);
|
||||
});
|
||||
|
||||
test('will schedule a restore', async () => {
|
||||
server.use(getOrganization);
|
||||
mocks.useGetPiTrBaseBackupsLazyQuery.mockImplementation(() => [
|
||||
fetchPiTRBaseBackups,
|
||||
{ loading: false },
|
||||
]);
|
||||
|
||||
await waitFor(() =>
|
||||
render(<PointInTimeBackupInfo appId={mockApplication.id} />),
|
||||
);
|
||||
const user = new TestUserEvent();
|
||||
const startRestoreButton = await screen.getByRole('button', {
|
||||
name: 'Start restore',
|
||||
});
|
||||
|
||||
await user.click(startRestoreButton);
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.getByText('Recover your database from a backup'),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
const dateTimePickerButton = await screen.getByRole('button', {
|
||||
name: /UTC/i,
|
||||
});
|
||||
await user.click(dateTimePickerButton);
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.getByRole('button', { name: 'Select' }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
await user.click(await screen.getByText('13'));
|
||||
|
||||
const hoursInput = await screen.getByLabelText('Hours');
|
||||
await user.type(hoursInput, '18');
|
||||
|
||||
const updatedDateTimeButton = await screen.getByRole('button', {
|
||||
name: /UTC/i,
|
||||
});
|
||||
expect(updatedDateTimeButton).toHaveTextContent(
|
||||
'13 Mar 2025, 18:00:05 (UTC+02:00)',
|
||||
);
|
||||
await user.click(await screen.getByRole('button', { name: 'Select' }));
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.queryByRole('button', { name: 'Select' }),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
|
||||
expect(updatedDateTimeButton).toHaveTextContent(
|
||||
'13 Mar 2025, 18:00:05 (UTC+02:00)',
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.getByRole('button', { name: 'Restore backup' }),
|
||||
).toBeDisabled();
|
||||
// check checkboxes
|
||||
|
||||
await user.click(
|
||||
await screen.getByLabelText(/I understand that restoring this backup/),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.getByLabelText(/I understand that restoring this backup/),
|
||||
).toBeChecked();
|
||||
|
||||
expect(
|
||||
await screen.getByRole('button', { name: 'Restore backup' }),
|
||||
).toBeDisabled();
|
||||
|
||||
await user.click(
|
||||
await screen.getByLabelText(/I understand this cannot be undone/),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.getByLabelText(/I understand this cannot be undone/),
|
||||
).toBeChecked();
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.getByRole('button', { name: 'Restore backup' }),
|
||||
).not.toBeDisabled(),
|
||||
);
|
||||
|
||||
await user.click(
|
||||
await screen.getByRole('button', { name: 'Restore backup' }),
|
||||
);
|
||||
|
||||
expect(
|
||||
mocks.restoreApplicationDatabase.mock.calls[0][0].fromAppId,
|
||||
).toBeNull();
|
||||
|
||||
expect(
|
||||
mocks.restoreApplicationDatabase.mock.calls[0][0].recoveryTarget,
|
||||
).toBe('2025-03-13T16:00:05.000Z');
|
||||
|
||||
// call the onCompleted cb
|
||||
await waitFor(() => mocks.restoreApplicationDatabase.mock.calls[0][1]());
|
||||
expect(
|
||||
screen.getByText('Backup has been scheduled successfully.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('that dates before the earliest backup cannot be selected', async () => {
|
||||
server.use(getOrganization);
|
||||
mocks.useGetPiTrBaseBackupsLazyQuery.mockImplementation(() => [
|
||||
fetchPiTRBaseBackups,
|
||||
{ loading: false },
|
||||
]);
|
||||
|
||||
await waitFor(() =>
|
||||
render(<PointInTimeBackupInfo appId={mockApplication.id} />),
|
||||
);
|
||||
const user = new TestUserEvent();
|
||||
const startRestoreButton = await screen.getByRole('button', {
|
||||
name: 'Start restore',
|
||||
});
|
||||
|
||||
await user.click(startRestoreButton);
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.getByText('Recover your database from a backup'),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
const dateTimePickerButton = await screen.getByRole('button', {
|
||||
name: /UTC/i,
|
||||
});
|
||||
|
||||
await user.click(dateTimePickerButton);
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.getByRole('button', { name: 'Select' }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
expect(await screen.getByText('March 2025')).toBeInTheDocument();
|
||||
|
||||
expect(await screen.getByText('9')).toBeDisabled();
|
||||
|
||||
expect(await screen.getAllByText('1')[0]).toBeDisabled();
|
||||
|
||||
expect(await screen.getAllByText('5')[0]).toBeDisabled();
|
||||
|
||||
expect(await screen.getByText('10')).not.toBeDisabled();
|
||||
|
||||
expect(await screen.getByText('15')).not.toBeDisabled();
|
||||
|
||||
const hoursInput = await screen.getByLabelText('Hours');
|
||||
await user.type(hoursInput, '{ArrowDown}');
|
||||
|
||||
expect(await screen.getByLabelText('Hours')).toHaveValue('04');
|
||||
|
||||
const updatedDateTimeButton = await screen.getByRole('button', {
|
||||
name: /UTC/i,
|
||||
});
|
||||
expect(updatedDateTimeButton).toHaveTextContent(
|
||||
'10 Mar 2025, 04:00:05 (UTC+02:00)',
|
||||
);
|
||||
expect(
|
||||
await screen.queryByRole('button', { name: 'Select' }),
|
||||
).toBeDisabled();
|
||||
|
||||
expect(
|
||||
await screen.queryByText(
|
||||
'Selected date and time is before the earliest available backup',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.getByRole('button', {
|
||||
name: /UTC/i,
|
||||
}),
|
||||
).toHaveClass('border-destructive');
|
||||
});
|
||||
|
||||
test('Learn more link is displayed in the "footer" and aligned to the left and the "Start restore" button is to the right', async () => {
|
||||
server.use(getOrganization);
|
||||
mocks.useGetPiTrBaseBackupsLazyQuery.mockImplementation(() => [
|
||||
fetchPiTRBaseBackups,
|
||||
{ loading: false },
|
||||
]);
|
||||
|
||||
await waitFor(() =>
|
||||
render(<PointInTimeBackupInfo appId={mockApplication.id} showLink />),
|
||||
);
|
||||
const learMoreAboutPiTRLink = screen.getByText(
|
||||
/Learn more about Point-in-Time Recover/,
|
||||
);
|
||||
expect(learMoreAboutPiTRLink).toBeInTheDocument();
|
||||
const linkWrapper = learMoreAboutPiTRLink.closest('div');
|
||||
expect(linkWrapper).toHaveClass('justify-between');
|
||||
expect(linkWrapper).not.toHaveClass('justify-end');
|
||||
});
|
||||
|
||||
test('Learn more link is in not displayed in the "footer" the "Start restore" button is aligned to the right', async () => {
|
||||
server.use(getOrganization);
|
||||
mocks.useGetPiTrBaseBackupsLazyQuery.mockImplementation(() => [
|
||||
fetchPiTRBaseBackups,
|
||||
{ loading: false },
|
||||
]);
|
||||
|
||||
await waitFor(() =>
|
||||
render(<PointInTimeBackupInfo appId={mockApplication.id} />),
|
||||
);
|
||||
expect(
|
||||
screen.queryByText(/Learn more about Point-in-Time Recover/),
|
||||
).not.toBeInTheDocument();
|
||||
const startRestoreButton = screen.getByText('Start restore');
|
||||
const linkWrapper = startRestoreButton.closest('div');
|
||||
expect(linkWrapper).not.toHaveClass('justify-between');
|
||||
expect(linkWrapper).toHaveClass('justify-end');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
import { mockMatchMediaValue } from '@/tests/mocks';
|
||||
import { render, screen, TestUserEvent } from '@/tests/testUtils';
|
||||
import { vi } from 'vitest';
|
||||
import DatabasePiTRSettings from './DatabasePiTRSettings';
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(mockMatchMediaValue),
|
||||
});
|
||||
|
||||
function getCurrentOrg({ isFree }: { isFree: boolean }) {
|
||||
return {
|
||||
org: {
|
||||
plan: {
|
||||
isFree,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mockUseGetPostgresSettingsQueryResponse({
|
||||
retention,
|
||||
}: {
|
||||
retention: number | null;
|
||||
}) {
|
||||
const pitr = retention === null ? null : { retention };
|
||||
return {
|
||||
data: {
|
||||
config: {
|
||||
postgres: {
|
||||
pitr,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useCurrentOrg: vi.fn(),
|
||||
useUpdateConfigMutation: vi.fn(),
|
||||
useGetPostgresSettingsQuery: vi.fn(),
|
||||
updateConfigMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useCurrentOrg', async () => {
|
||||
const actualCurrentOrg = await vi.importActual<any>(
|
||||
'@/features/orgs/projects/hooks/useCurrentOrg',
|
||||
);
|
||||
return {
|
||||
...actualCurrentOrg,
|
||||
useCurrentOrg: mocks.useCurrentOrg,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/utils/__generated__/graphql', async () => {
|
||||
const actual = await vi.importActual<any>('@/utils/__generated__/graphql');
|
||||
return {
|
||||
...actual,
|
||||
useUpdateConfigMutation: mocks.useUpdateConfigMutation,
|
||||
useGetPostgresSettingsQuery: mocks.useGetPostgresSettingsQuery,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mocks.useCurrentOrg.mockRestore();
|
||||
mocks.updateConfigMock.mockRestore();
|
||||
mocks.useUpdateConfigMutation.mockRestore();
|
||||
mocks.useGetPostgresSettingsQuery.mockRestore();
|
||||
});
|
||||
|
||||
test('If the org is free the switch should not be available and the save button is disabled', async () => {
|
||||
mocks.useCurrentOrg.mockImplementation(() => getCurrentOrg({ isFree: true }));
|
||||
mocks.useUpdateConfigMutation.mockImplementation(() => [
|
||||
mocks.updateConfigMock,
|
||||
]);
|
||||
mocks.useGetPostgresSettingsQuery.mockImplementation(() =>
|
||||
mockUseGetPostgresSettingsQueryResponse({ retention: null }),
|
||||
);
|
||||
render(<DatabasePiTRSettings />);
|
||||
const saveButton = await screen.findByRole('button', {
|
||||
name: 'Save',
|
||||
});
|
||||
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
expect(await screen.queryByRole('checkbox')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('the Save button is disabled until the switch in the header is not touched', async () => {
|
||||
mocks.useCurrentOrg.mockImplementation(() =>
|
||||
getCurrentOrg({ isFree: false }),
|
||||
);
|
||||
mocks.useUpdateConfigMutation.mockImplementation(() => [
|
||||
mocks.updateConfigMock,
|
||||
]);
|
||||
mocks.useGetPostgresSettingsQuery.mockImplementation(() =>
|
||||
mockUseGetPostgresSettingsQueryResponse({ retention: null }),
|
||||
);
|
||||
const user = new TestUserEvent();
|
||||
render(<DatabasePiTRSettings />);
|
||||
const saveButton = await screen.findByRole('button', {
|
||||
name: 'Save',
|
||||
});
|
||||
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const PiTR = screen.getByRole('checkbox');
|
||||
await user.click(PiTR);
|
||||
expect(PiTR).toBeChecked();
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
name: 'Save',
|
||||
}),
|
||||
).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test('should disable the savebutton after toggling back to original state', async () => {
|
||||
mocks.useCurrentOrg.mockImplementation(() =>
|
||||
getCurrentOrg({ isFree: false }),
|
||||
);
|
||||
mocks.useUpdateConfigMutation.mockImplementation(() => [
|
||||
mocks.updateConfigMock,
|
||||
]);
|
||||
mocks.useGetPostgresSettingsQuery.mockImplementation(() =>
|
||||
mockUseGetPostgresSettingsQueryResponse({ retention: 7 }),
|
||||
);
|
||||
const user = new TestUserEvent();
|
||||
render(<DatabasePiTRSettings />);
|
||||
|
||||
await user.click(screen.getByRole('checkbox'));
|
||||
expect(screen.getByRole('checkbox')).not.toBeChecked();
|
||||
await user.click(screen.getByRole('checkbox'));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
name: 'Save',
|
||||
}),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should send { retention: 7 } when enabling PiTR', async () => {
|
||||
mocks.useCurrentOrg.mockImplementation(() =>
|
||||
getCurrentOrg({ isFree: false }),
|
||||
);
|
||||
mocks.useUpdateConfigMutation.mockImplementation(() => [
|
||||
mocks.updateConfigMock,
|
||||
]);
|
||||
mocks.useGetPostgresSettingsQuery.mockImplementation(() =>
|
||||
mockUseGetPostgresSettingsQueryResponse({ retention: null }),
|
||||
);
|
||||
const user = new TestUserEvent();
|
||||
render(<DatabasePiTRSettings />);
|
||||
await user.click(screen.getByRole('checkbox'));
|
||||
expect(screen.getByRole('checkbox')).toBeChecked();
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Save',
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
mocks.updateConfigMock.mock.calls[0][0].variables.config.postgres.pitr,
|
||||
).toStrictEqual({ retention: 7 });
|
||||
});
|
||||
|
||||
test('should send { pitr: null } when disabling PiTR', async () => {
|
||||
mocks.useCurrentOrg.mockImplementation(() =>
|
||||
getCurrentOrg({ isFree: false }),
|
||||
);
|
||||
mocks.useUpdateConfigMutation.mockImplementation(() => [
|
||||
mocks.updateConfigMock,
|
||||
]);
|
||||
mocks.useGetPostgresSettingsQuery.mockImplementation(() =>
|
||||
mockUseGetPostgresSettingsQueryResponse({ retention: 7 }),
|
||||
);
|
||||
const user = new TestUserEvent();
|
||||
render(<DatabasePiTRSettings />);
|
||||
await user.click(screen.getByRole('checkbox'));
|
||||
expect(screen.getByRole('checkbox')).not.toBeChecked();
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Save',
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
mocks.updateConfigMock.mock.calls[0][0].variables.config.postgres,
|
||||
).toStrictEqual({ pitr: null });
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mockMatchMediaValue, mockRouter } from '@/tests/mocks';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import { getProPlanOnlyQuery } from '@/tests/msw/mocks/graphql/plansQuery';
|
||||
import {
|
||||
resourcesAvailableQuery,
|
||||
@@ -8,12 +9,10 @@ import {
|
||||
import updateConfigMutation from '@/tests/msw/mocks/graphql/updateConfigMutation';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import {
|
||||
clickOnElement,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userClearElement,
|
||||
userType,
|
||||
TestUserEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
RESOURCE_MEMORY_MULTIPLIER,
|
||||
RESOURCE_VCPU_MULTIPLIER,
|
||||
} from '@/utils/constants/common';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import ResourcesForm from './ResourcesForm';
|
||||
@@ -39,6 +37,7 @@ vi.mock('next/router', () => ({
|
||||
const server = setupServer(
|
||||
tokenQuery,
|
||||
resourcesAvailableQuery,
|
||||
getProjectQuery,
|
||||
getProPlanOnlyQuery,
|
||||
);
|
||||
|
||||
@@ -54,9 +53,14 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
// Note: Workaround based on https://github.com/testing-library/user-event/issues/871#issuecomment-1059317998
|
||||
function changeSliderValue(slider: HTMLElement, value: number) {
|
||||
fireEvent.input(slider, { target: { value } });
|
||||
fireEvent.change(slider, { target: { value } });
|
||||
async function changeSliderValue(slider: HTMLElement, value: number) {
|
||||
await waitFor(() => {
|
||||
fireEvent.input(slider, { target: { value } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.change(slider, { target: { value } });
|
||||
});
|
||||
}
|
||||
|
||||
test('should show an empty state message that the feature must be enabled if no data is available', async () => {
|
||||
@@ -69,7 +73,7 @@ test('should show an empty state message that the feature must be enabled if no
|
||||
|
||||
test('should show the sliders if the switch is enabled', async () => {
|
||||
server.use(resourcesUnavailableQuery);
|
||||
const user = userEvent.setup();
|
||||
const user = new TestUserEvent();
|
||||
|
||||
render(<ResourcesForm />);
|
||||
|
||||
@@ -77,8 +81,10 @@ test('should show the sliders if the switch is enabled', async () => {
|
||||
|
||||
await user.click(screen.getByRole('checkbox'));
|
||||
|
||||
// Wait for the empty state message to disappear and sliders to appear
|
||||
expect(screen.queryByText(/enable this feature/i)).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole('slider')).toHaveLength(9);
|
||||
const sliders = screen.getAllByRole('slider');
|
||||
expect(sliders).toHaveLength(9);
|
||||
});
|
||||
|
||||
test('should not show an empty state message if there is data available', async () => {
|
||||
@@ -101,12 +107,14 @@ test('should show a warning message if not all the resources are allocated', asy
|
||||
await screen.findByRole('slider', { name: /total available vcpu/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', {
|
||||
name: /total available vcpu/i,
|
||||
}),
|
||||
9 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
await waitFor(() => {
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', {
|
||||
name: /total available vcpu/i,
|
||||
}),
|
||||
9 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText(/^vcpus:/i)).toHaveTextContent(/vcpus: 9/i);
|
||||
expect(screen.getByText(/^memory:/i)).toHaveTextContent(/memory: 18432 mib/i);
|
||||
@@ -136,6 +144,7 @@ test('should update the price when the top slider is changed', async () => {
|
||||
});
|
||||
|
||||
test('should show a validation error when the form is submitted when not everything is allocated', async () => {
|
||||
const user = new TestUserEvent();
|
||||
render(<ResourcesForm />);
|
||||
|
||||
expect(
|
||||
@@ -151,7 +160,7 @@ test('should show a validation error when the form is submitted when not everyth
|
||||
9 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
|
||||
await clickOnElement(screen.getByRole('button', { name: /save/i }));
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
expect(
|
||||
screen.getByText(/you have 1 vcpus and 2048 mib of memory unused./i),
|
||||
@@ -161,6 +170,7 @@ test('should show a validation error when the form is submitted when not everyth
|
||||
});
|
||||
|
||||
test('should show a confirmation dialog when the form is submitted', async () => {
|
||||
const user = new TestUserEvent();
|
||||
server.use(updateConfigMutation);
|
||||
render(<ResourcesForm />);
|
||||
|
||||
@@ -209,7 +219,7 @@ test('should show a confirmation dialog when the form is submitted', async () =>
|
||||
5 * RESOURCE_MEMORY_MULTIPLIER,
|
||||
);
|
||||
|
||||
await clickOnElement(screen.getByRole('button', { name: /save/i }));
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
expect(
|
||||
@@ -239,7 +249,7 @@ test('should show a confirmation dialog when the form is submitted', async () =>
|
||||
// and we need to return the updated values
|
||||
server.use(resourcesUpdatedQuery);
|
||||
|
||||
await clickOnElement(screen.getByRole('button', { name: /confirm/i }));
|
||||
await user.click(screen.getByRole('button', { name: /confirm/i }));
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
|
||||
|
||||
@@ -254,19 +264,20 @@ test('should show a confirmation dialog when the form is submitted', async () =>
|
||||
});
|
||||
|
||||
test('should display a red button when custom resources are disabled', async () => {
|
||||
const user = new TestUserEvent();
|
||||
render(<ResourcesForm />);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('slider', { name: /total available vcpu/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await clickOnElement(screen.getAllByRole('checkbox')[0]);
|
||||
await user.click(screen.getAllByRole('checkbox')[0]);
|
||||
|
||||
await waitFor(() => {});
|
||||
|
||||
expect(screen.getByText(/enable this feature/i)).toBeInTheDocument();
|
||||
|
||||
await clickOnElement(screen.getByRole('button', { name: /save/i }));
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
@@ -279,6 +290,7 @@ test('should display a red button when custom resources are disabled', async ()
|
||||
});
|
||||
|
||||
test('should hide the pricing information when custom resource allocation is disabled', async () => {
|
||||
const user = new TestUserEvent();
|
||||
server.use(updateConfigMutation);
|
||||
|
||||
render(<ResourcesForm />);
|
||||
@@ -287,15 +299,15 @@ test('should hide the pricing information when custom resource allocation is dis
|
||||
await screen.findByRole('slider', { name: /total available vcpu/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await clickOnElement(screen.getAllByRole('checkbox')[0]);
|
||||
await user.click(screen.getAllByRole('checkbox')[0]);
|
||||
|
||||
await clickOnElement(screen.getByRole('button', { name: /save/i }));
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
server.use(resourcesUnavailableQuery);
|
||||
|
||||
await clickOnElement(screen.getByRole('button', { name: /confirm/i }));
|
||||
await user.click(screen.getByRole('button', { name: /confirm/i }));
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
|
||||
|
||||
@@ -324,6 +336,7 @@ test('should show a warning message when resources are overallocated', async ()
|
||||
});
|
||||
|
||||
test('should change pricing based on selected replicas', async () => {
|
||||
const user = new TestUserEvent();
|
||||
render(<ResourcesForm />);
|
||||
|
||||
expect(
|
||||
@@ -336,9 +349,9 @@ test('should change pricing based on selected replicas', async () => {
|
||||
|
||||
const hasuraReplicasInput = screen.getAllByPlaceholderText('Replicas')[0];
|
||||
|
||||
await clickOnElement(hasuraReplicasInput);
|
||||
await userClearElement(hasuraReplicasInput);
|
||||
await userType(hasuraReplicasInput, '2');
|
||||
await user.click(hasuraReplicasInput);
|
||||
await user.clear(hasuraReplicasInput);
|
||||
await user.type(hasuraReplicasInput, '2');
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
@@ -350,9 +363,9 @@ test('should change pricing based on selected replicas', async () => {
|
||||
),
|
||||
);
|
||||
|
||||
await clickOnElement(hasuraReplicasInput);
|
||||
await userClearElement(hasuraReplicasInput);
|
||||
await userType(hasuraReplicasInput, '1');
|
||||
await user.click(hasuraReplicasInput);
|
||||
await user.clear(hasuraReplicasInput);
|
||||
await user.type(hasuraReplicasInput, '1');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/approximate cost:/i)).toHaveTextContent(
|
||||
@@ -362,7 +375,7 @@ test('should change pricing based on selected replicas', async () => {
|
||||
});
|
||||
|
||||
test('should validate if vCPU and Memory match the 1:2 ratio if more than 1 replica is selected', async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = new TestUserEvent();
|
||||
|
||||
render(<ResourcesForm />);
|
||||
|
||||
@@ -411,18 +424,18 @@ test('should validate if vCPU and Memory match the 1:2 ratio if more than 1 repl
|
||||
});
|
||||
|
||||
test('should take replicas into account when confirming the resources', async () => {
|
||||
const user = new TestUserEvent();
|
||||
server.use(updateConfigMutation);
|
||||
render(<ResourcesForm />);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('slider', { name: /total available vcpu/i }),
|
||||
).toBeInTheDocument();
|
||||
// Wait for initial render
|
||||
const totalVCPUSlider = await screen.findByRole('slider', {
|
||||
name: /total available vcpu/i,
|
||||
});
|
||||
expect(totalVCPUSlider).toBeInTheDocument();
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', {
|
||||
name: /total available vcpu/i,
|
||||
}),
|
||||
8.5 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
// Change slider values and wait for updates
|
||||
changeSliderValue(totalVCPUSlider, 8.5 * RESOURCE_VCPU_MULTIPLIER);
|
||||
|
||||
// setting up database
|
||||
changeSliderValue(
|
||||
@@ -435,9 +448,9 @@ test('should take replicas into account when confirming the resources', async ()
|
||||
);
|
||||
|
||||
const hasuraReplicasInput = screen.getAllByPlaceholderText('Replicas')[0];
|
||||
await clickOnElement(hasuraReplicasInput);
|
||||
await userClearElement(hasuraReplicasInput);
|
||||
await userType(hasuraReplicasInput, '3');
|
||||
await user.click(hasuraReplicasInput);
|
||||
await user.clear(hasuraReplicasInput);
|
||||
await user.type(hasuraReplicasInput, '3');
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /hasura graphql vcpu/i }),
|
||||
@@ -450,9 +463,9 @@ test('should take replicas into account when confirming the resources', async ()
|
||||
|
||||
const authReplicasInput = screen.getAllByPlaceholderText('Replicas')[1];
|
||||
// setting up auth
|
||||
await clickOnElement(authReplicasInput);
|
||||
await userClearElement(authReplicasInput);
|
||||
await userType(authReplicasInput, '2');
|
||||
await user.click(authReplicasInput);
|
||||
await user.clear(authReplicasInput);
|
||||
await user.type(authReplicasInput, '2');
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /auth vcpu/i }),
|
||||
@@ -465,9 +478,9 @@ test('should take replicas into account when confirming the resources', async ()
|
||||
|
||||
const storageReplicasInput = screen.getAllByPlaceholderText('Replicas')[2];
|
||||
// setting up storage
|
||||
await clickOnElement(storageReplicasInput);
|
||||
await userClearElement(storageReplicasInput);
|
||||
await userType(storageReplicasInput, '4');
|
||||
await user.click(storageReplicasInput);
|
||||
await user.clear(storageReplicasInput);
|
||||
await user.type(storageReplicasInput, '4');
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /storage vcpu/i }),
|
||||
@@ -478,11 +491,12 @@ test('should take replicas into account when confirming the resources', async ()
|
||||
5 * RESOURCE_MEMORY_MULTIPLIER,
|
||||
);
|
||||
|
||||
await clickOnElement(screen.getByRole('button', { name: /save/i }));
|
||||
// Click save and wait for dialog
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
const dialog = screen.getByRole('dialog');
|
||||
// Wait for dialog to appear
|
||||
const dialog = await screen.findByRole('dialog', {}, { timeout: 5000 });
|
||||
expect(dialog).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
within(dialog).getByText(/postgresql database/i).parentElement,
|
||||
@@ -500,8 +514,6 @@ test('should take replicas into account when confirming the resources', async ()
|
||||
/2\.5 vcpu \+ 5120 mib \(4 replicas\)/i,
|
||||
);
|
||||
|
||||
// total must contain the sum of all resources when replicas are taken into
|
||||
// account
|
||||
expect(within(dialog).getByText(/total/i).parentElement).toHaveTextContent(
|
||||
/22\.5 vcpu \+ 46080 mib/i,
|
||||
);
|
||||
|
||||
@@ -43,6 +43,12 @@ export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
cpu: 2000,
|
||||
memory: 4096,
|
||||
},
|
||||
enablePublicAccess: null,
|
||||
storage: {
|
||||
capacity: 1,
|
||||
},
|
||||
autoscaler: null,
|
||||
networking: null,
|
||||
replicas: 1,
|
||||
},
|
||||
},
|
||||
@@ -52,6 +58,8 @@ export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
cpu: 2000,
|
||||
memory: 4096,
|
||||
},
|
||||
autoscaler: null,
|
||||
networking: null,
|
||||
replicas: 1,
|
||||
},
|
||||
},
|
||||
@@ -61,6 +69,8 @@ export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
cpu: 2000,
|
||||
memory: 4096,
|
||||
},
|
||||
autoscaler: null,
|
||||
networking: null,
|
||||
replicas: 1,
|
||||
},
|
||||
},
|
||||
@@ -70,6 +80,8 @@ export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
cpu: 2000,
|
||||
memory: 4096,
|
||||
},
|
||||
autoscaler: null,
|
||||
networking: null,
|
||||
replicas: 1,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { DialogProvider } from '@/components/common/DialogProvider';
|
||||
import { UIProvider } from '@/components/common/UIProvider';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
@@ -22,7 +23,10 @@ import {
|
||||
waitForElementToBeRemoved as rtlWaitForElementToBeRemoved,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import userEvent, {
|
||||
type Options,
|
||||
type UserEvent,
|
||||
} from '@testing-library/user-event';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
@@ -32,6 +36,14 @@ import nhostGraphQLLink from './msw/mocks/graphql/nhostGraphQLLink';
|
||||
// Client-side cache, shared for the whole session of the user in the browser.
|
||||
const emotionCache = createEmotionCache();
|
||||
|
||||
/* Workaround to avoid error importing type declarations for typeOptions from @testing-library/user-event */
|
||||
export interface TypeOptions {
|
||||
skipClick?: Options['skipClick'];
|
||||
skipAutoClose?: Options['skipAutoClose'];
|
||||
initialSelectionStart?: number;
|
||||
initialSelectionEnd?: number;
|
||||
}
|
||||
|
||||
process.env = {
|
||||
TEST_MODE: 'true',
|
||||
NODE_ENV: 'development',
|
||||
@@ -162,25 +174,30 @@ export const mockPointerEvent = () => {
|
||||
window.HTMLElement.prototype.hasPointerCapture = vi.fn();
|
||||
};
|
||||
|
||||
export async function clickOnElement(element: Element) {
|
||||
const user = userEvent.setup();
|
||||
await waitFor(async () => {
|
||||
await user.click(element);
|
||||
});
|
||||
}
|
||||
export class TestUserEvent {
|
||||
private user: UserEvent;
|
||||
|
||||
export async function userType(element: Element, value: string, options?: any) {
|
||||
const user = userEvent.setup();
|
||||
await waitFor(async () => {
|
||||
await user.type(element, value, options);
|
||||
});
|
||||
}
|
||||
constructor() {
|
||||
this.user = userEvent.setup();
|
||||
}
|
||||
|
||||
export async function userClearElement(element: Element) {
|
||||
const user = userEvent.setup();
|
||||
await waitFor(async () => {
|
||||
await user.clear(element);
|
||||
});
|
||||
async click(element: Element) {
|
||||
await waitFor(async () => {
|
||||
await this.user.click(element);
|
||||
});
|
||||
}
|
||||
|
||||
async type(element: Element, value: string, options?: TypeOptions) {
|
||||
await waitFor(async () => {
|
||||
await this.user.type(element, value, options);
|
||||
});
|
||||
}
|
||||
|
||||
async clear(element: Element) {
|
||||
await waitFor(async () => {
|
||||
await this.user.clear(element);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export * from '@testing-library/react';
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"install-browsers": "pnpm playwright install && pnpm playwright install-deps",
|
||||
"test": "pnpm install-browsers && pnpm playwright test",
|
||||
"e2e": "pnpm install-browsers && pnpm playwright test",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user