Skip to content

Commit

Permalink
Add settings page test
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed May 21, 2024
1 parent efc50b9 commit e275876
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 18 deletions.
12 changes: 9 additions & 3 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import type { LoaderFunctionArgs } from '@remix-run/node';
import { Links, Meta, Outlet, Scripts, useLoaderData } from '@remix-run/react';
import { getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit';

Check warning on line 3 in app/root.tsx

View check run for this annotation

Codecov / codecov/patch

app/root.tsx#L3

Added line #L3 was not covered by tests
import { Authenticator } from 'remix-auth';
import type { SessionData } from './auth/session-context';
import { SessionProvider } from './auth/session-context';
import { MainHeader } from './common/MainHeader';
import { serverContainer } from './container/container.server';
import { appDataSource } from './db/data-source.server';
import { SettingsService } from './settings/SettingsService.server';

Check warning on line 10 in app/root.tsx

View check run for this annotation

Codecov / codecov/patch

app/root.tsx#L10

Added line #L10 was not covered by tests
import './index.scss';

export async function loader(
{ request }: LoaderFunctionArgs,
authenticator: Authenticator<SessionData> = serverContainer[Authenticator.name],
settingsService: SettingsService = serverContainer[SettingsService.name],

Check warning on line 16 in app/root.tsx

View check run for this annotation

Codecov / codecov/patch

app/root.tsx#L16

Added line #L16 was not covered by tests
) {
// FIXME This should be done during server start-up, not here
if (!appDataSource.isInitialized) {
Expand All @@ -27,14 +30,17 @@ export async function loader(
request,
{ failureRedirect: `/login?redirect-to=${encodeURIComponent(pathname)}` },
));
return { session };

const settings = session && await settingsService.userSettings(session.userId);

return { session, settings };

Check warning on line 36 in app/root.tsx

View check run for this annotation

Codecov / codecov/patch

app/root.tsx#L33-L36

Added lines #L33 - L36 were not covered by tests
}

export default function App() {
const { session } = useLoaderData<typeof loader>();
const { session, settings } = useLoaderData<typeof loader>();

Check warning on line 40 in app/root.tsx

View check run for this annotation

Codecov / codecov/patch

app/root.tsx#L40

Added line #L40 was not covered by tests

return (
<html lang="en">
<html lang="en" data-theme={settings?.ui?.theme ?? getSystemPreferredTheme()}>

Check warning on line 43 in app/root.tsx

View check run for this annotation

Codecov / codecov/patch

app/root.tsx#L43

Added line #L43 was not covered by tests
<head>
<title>Shlink dashboard</title>

Expand Down
23 changes: 8 additions & 15 deletions app/routes/settings.$.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
import { useFetcher, useLoaderData } from '@remix-run/react';
import type { Settings as AppSettings } from '@shlinkio/shlink-web-component/settings';
import { type ReactNode, useCallback, useEffect, useState } from 'react';
import { ShlinkWebSettings } from '@shlinkio/shlink-web-component/settings';
import { useCallback } from 'react';
import { Authenticator } from 'remix-auth';
import type { SessionData } from '../auth/session-context';
import { serverContainer } from '../container/container.server';
import { SettingsService } from '../settings/SettingsService.server';

export async function loader(
{ request }: LoaderFunctionArgs,
settingsService: SettingsService = serverContainer[SettingsService.name],
authenticator: Authenticator<SessionData> = serverContainer[Authenticator.name],
settingsService: SettingsService = serverContainer[SettingsService.name],
) {
const { userId } = await authenticator.isAuthenticated(request, { failureRedirect: '/login' });
return settingsService.userSettings(userId);
}

export async function action(
{ request }: ActionFunctionArgs,
settingsService: SettingsService = serverContainer[SettingsService.name],
authenticator: Authenticator<SessionData> = serverContainer[Authenticator.name],
settingsService: SettingsService = serverContainer[SettingsService.name],
) {
const [sessionData, newSettings] = await Promise.all([
authenticator.isAuthenticated(request),
Expand All @@ -30,12 +31,10 @@ export async function action(
}

await settingsService.saveUserSettings(sessionData.userId, newSettings);

return {};
}

export default function Settings() {
const [component, setComponent] = useState<ReactNode>(null);
const settings = useLoaderData<typeof loader>();
const fetcher = useFetcher();
// TODO Add some deferring
Expand All @@ -44,19 +43,13 @@ export default function Settings() {
encType: 'application/json',
}), [fetcher]);

useEffect(() => {
import('@shlinkio/shlink-web-component/settings').then(({ ShlinkWebSettings }) => setComponent(
return (
<div className="tw-container lg:tw-p-5 tw-p-3 mx-auto">
<ShlinkWebSettings
settings={settings}
defaultShortUrlsListOrdering={{}}
updateSettings={submitSettings}
/>,
));
}, [submitSettings, settings]);

return (
<div className="tw-container lg:tw-p-5 tw-p-3 mx-auto">
{component}
defaultShortUrlsListOrdering={{}}
/>
</div>
);
}

Check warning on line 55 in app/routes/settings.$.tsx

View check run for this annotation

Codecov / codecov/patch

app/routes/settings.$.tsx#L37-L55

Added lines #L37 - L55 were not covered by tests
83 changes: 83 additions & 0 deletions test/routes/settings.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { ActionFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { createRemixStub } from '@remix-run/testing';
import type { Settings } from '@shlinkio/shlink-web-component/settings';
import { render, screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import type { Authenticator } from 'remix-auth';
import type { SessionData } from '../../app/auth/session-context';
import SettingsComp, { action as settingsAction, loader } from '../../app/routes/settings.$';
import type { SettingsService } from '../../app/settings/SettingsService.server';

describe('settings', () => {
const isAuthenticated = vi.fn();
const authenticator = fromPartial<Authenticator<SessionData>>({ isAuthenticated });
const userSettings = vi.fn();
const saveUserSettings = vi.fn();
const settingsService = fromPartial<SettingsService>({ userSettings, saveUserSettings });

describe('loader', () => {
it('checks if user is authenticated and returns their settings', async () => {
const settings = fromPartial<Settings>({
ui: { theme: 'dark' },
});
userSettings.mockResolvedValue(settings);
isAuthenticated.mockResolvedValue({ userId: 1 });

const result = await loader(fromPartial({ request: {} }), authenticator, settingsService);

expect(result).toEqual(settings);
expect(userSettings).toHaveBeenCalled();
expect(isAuthenticated).toHaveBeenCalled();
});
});

describe('action', () => {
const setUp = () => (args: ActionFunctionArgs) => settingsAction(args, authenticator, settingsService);
const request = fromPartial<Request>({ json: vi.fn().mockResolvedValue({}) });

it('does not save settings when user is not logged in', async () => {
const action = setUp();

isAuthenticated.mockResolvedValue(null);

await action(fromPartial({ request }));

expect(isAuthenticated).toHaveBeenCalledWith(request);
expect(saveUserSettings).not.toHaveBeenCalled();
});

it('saves settings when user is logged in', async () => {
const action = setUp();

isAuthenticated.mockResolvedValue({ userId: 1 });

await action(fromPartial({ request }));

expect(isAuthenticated).toHaveBeenCalledWith(request);
expect(saveUserSettings).toHaveBeenCalledWith(1, {});
});
});

// Skipping for now, as createRemixStub always results in a 404 page
describe.skip('<Settings />', () => {
const setUp = () => {
const RemixStub = createRemixStub([
{
path: '/settings',
Component: SettingsComp,
loader: () => json({}),
action: () => json({}),
},
]);
return render(<RemixStub />);
};

it('renders settings component', async () => {
setUp();

await waitFor(() => expect(screen.getByRole('heading', { name: 'User interface' })).toBeInTheDocument());
expect(screen.getByRole('heading', { name: 'Real-time updates' })).toBeInTheDocument();
});
});
});

0 comments on commit e275876

Please sign in to comment.