Skip to content

Commit

Permalink
chore: adds tests as example and for plumbing different things (#8)
Browse files Browse the repository at this point in the history
* chore: adds tests for cookie context

* chore: adds page test and serverside props
  • Loading branch information
mikaelbr authored Sep 21, 2023
1 parent 00fafc4 commit 1c03c6e
Show file tree
Hide file tree
Showing 11 changed files with 892 additions and 44 deletions.
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@
},
"devDependencies": {
"@atb-as/generate-assets": "^9.0.0",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1",
"@types/bunyan": "^1.8.9",
"@types/lodash": "^4.14.198",
"@types/uuid": "^9.0.4",
"@vitejs/plugin-react": "^4.0.4",
"eslint": "8.49.0",
"eslint-config-next": "13.5.1",
"happy-dom": "^12.1.5",
"prettier": "^3.0.3",
"vitest": "^0.34.4",
"rimraf": "^5.0.1"
"rimraf": "^5.0.1",
"vitest": "^0.34.4"
}
}
4 changes: 1 addition & 3 deletions src/layouts/global-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ export function withGlobalData<P extends {} = {}>(
const initialData = getGlobalCookies(ctx.req);

const composedProps: GetServerSidePropsResult<P> | undefined =
await propGetter?.({
...ctx,
});
await propGetter?.(ctx);

if (!composedProps) {
return { props: initialData as WithGlobalData<P> };
Expand Down
2 changes: 1 addition & 1 deletion src/modules/api-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export function createWithHttpClientDecorator<U extends HttpEndpoints, T>(
ctx: GetServerSidePropsContext,
): Promise<GetServerSidePropsResult<P>> {
return propGetter({
...ctx,
client,
...ctx,
});
};
};
Expand Down
150 changes: 150 additions & 0 deletions src/modules/cookies/__tests__/cookies-context.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { render, cleanup } from '@testing-library/react';
import { afterEach, expect, it, describe } from 'vitest';
import userEvent from '@testing-library/user-event';
import {
AppCookiesProvider,
AppCookiesProviderProps,
useCookieSettings,
useDarkmodeCookie,
useLanguageCookie,
} from '../cookies-context';

afterEach(function () {
cleanup();
});

type RenderProps = { providerProps: AppCookiesProviderProps };
const customRender = (
ui: React.ReactNode,
{ providerProps, ...renderOptions }: RenderProps,
) => {
return render(
<AppCookiesProvider {...providerProps}>{ui}</AppCookiesProvider>,
renderOptions,
);
};

describe('cookie contexts', function () {
it('Should provide default state as passed in', () => {
const providerProps: AppCookiesProviderProps = {
initialCookies: {
darkmode: true,
language: 'no',
},
};
const TestConsumer = function () {
const state = useCookieSettings();

return (
<div>
<p>Darkmode: {String(state?.darkmode[0]) ?? ''}</p>
<p>Language: {state?.language[0] ?? ''}</p>
</div>
);
};

const output = customRender(<TestConsumer />, { providerProps });
expect(output.getByText(/^Darkmode:/).textContent).toBe('Darkmode: true');
expect(output.getByText(/^Language:/).textContent).toBe('Language: no');
});

describe('language', function () {
it('Should language cookie', () => {
const providerProps: AppCookiesProviderProps = {
initialCookies: {
darkmode: true,
language: 'no',
},
};
const TestConsumer = function () {
const [lang] = useLanguageCookie();

return (
<div>
<p>Language: {lang ?? ''}</p>
</div>
);
};

const output = customRender(<TestConsumer />, { providerProps });
expect(output.getByText(/^Language:/).textContent).toBe('Language: no');
});

it('Should language cookie', async () => {
const providerProps: AppCookiesProviderProps = {
initialCookies: {
darkmode: true,
language: 'no',
},
};
const TestConsumer = function () {
const [lang, setLanguage] = useLanguageCookie();

return (
<div>
<p>Language: {lang ?? ''}</p>
<button onClick={() => setLanguage('en')}>Change</button>
</div>
);
};

const output = customRender(<TestConsumer />, { providerProps });
expect(output.getByText(/^Language:/).textContent).toBe('Language: no');

const user = userEvent.setup();
await user.click(output.getByRole('button'));
expect(output.getByText(/^Language:/).textContent).toBe('Language: en');
});
});

describe('darkmode', function () {
it('Should darkmode cookie', () => {
const providerProps: AppCookiesProviderProps = {
initialCookies: {
darkmode: true,
language: 'no',
},
};
const TestConsumer = function () {
const [darkmode] = useDarkmodeCookie();

return (
<div>
<p>Darkmode: {String(darkmode) ?? ''}</p>
</div>
);
};

const output = customRender(<TestConsumer />, { providerProps });
expect(output.getByText(/^Darkmode:/).textContent).toBe('Darkmode: true');
});

it('Should language cookie', async () => {
const providerProps: AppCookiesProviderProps = {
initialCookies: {
darkmode: true,
language: 'no',
},
};
const TestConsumer = function () {
const [darkmode, setDarkmode] = useDarkmodeCookie();

return (
<div>
<p>Darkmode: {String(darkmode) ?? ''}</p>
<button onClick={() => setDarkmode(false)}>Change</button>
</div>
);
};

const output = customRender(<TestConsumer />, { providerProps });
expect(output.getByText(/^Darkmode:/).textContent).toBe('Darkmode: true');

const user = userEvent.setup();
await user.click(output.getByRole('button'));
expect(output.getByText(/^Darkmode:/).textContent).toBe(
'Darkmode: false',
);
});
});
});
31 changes: 7 additions & 24 deletions src/modules/cookies/cookies-context.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import { getCookie, setCookie } from 'cookies-next';
import { addDays } from 'date-fns';
import {
type PropsWithChildren,
createContext,
PropsWithChildren,
useCallback,
useContext,
useState,
} from 'react';

import {
DARKMODE_COOKIE_NAME,
LANGUAGE_COOKIE_NAME,
SETTINGS_STORETIME_DAYS,
} from './constants';

type CookieState = {
export type AppCookiesContextState = {
darkmode: [boolean | undefined, (val: boolean) => void];
language: [string | undefined, (val: string) => void];
};

const CookiesContext = createContext<CookieState | undefined>(undefined);
const CookiesContext = createContext<AppCookiesContextState | undefined>(
undefined,
);

export type InitialCookieData = {
darkmode: boolean | null;
language: string | null;
};

type AppCookiesProviderProps = PropsWithChildren<{
export type AppCookiesProviderProps = PropsWithChildren<{
initialCookies: InitialCookieData;
}>;

Expand All @@ -51,26 +54,6 @@ export function AppCookiesProvider({
);
}

export function AppCookiesTestProvider({
children,
initialCookies,
}: AppCookiesProviderProps) {
const language: CookieState['language'] = [
initialCookies.language ?? undefined,
(val: string) => {},
];
const darkmode: CookieState['darkmode'] = [
initialCookies.darkmode ?? undefined,
(val: boolean) => {},
];

return (
<CookiesContext.Provider value={{ language, darkmode }}>
{children}
</CookiesContext.Provider>
);
}

export function useCookieSettings() {
const context = useContext(CookiesContext);
return context;
Expand Down
58 changes: 58 additions & 0 deletions src/page-modules/departures/__tests__/departure.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { cleanup, render } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { HttpClient } from '@atb/modules/api-server';
import { AutocompleteApi } from '@atb/page-modules/departures/server/autocomplete';
import { expectProps, getServerSidePropsWithClient } from '@atb/tests/utils';

import DeparturesPage, {
DeparturesPageProps,
getServerSideProps,
} from '@atb/pages/departures';

afterEach(function () {
cleanup();
});

describe('departure page', function () {
it('Should show list as provided from initial props ', async () => {
const initialProps: DeparturesPageProps = {
autocompleteFeatures: [
{
name: 'Result 1',
},
{
name: 'Result 2',
},
],
headersAcceptLanguage: 'en',
initialCookies: {
darkmode: false,
language: 'en',
},
};

const output = render(<DeparturesPage {...initialProps} />);
expect(await output.findAllByText(/^Result/)).toHaveLength(2);
});

it('Should return props from getServerSideProps', async () => {
const expectedResult = [{ name: 'Test' }];

const client: HttpClient<'entur', AutocompleteApi> = {
async autocomplete() {
return expectedResult;
},
async request() {
return new Response();
},
};
const result = await getServerSidePropsWithClient(
client,
getServerSideProps,
);

(await expectProps(result)).toContain({
autocompleteFeatures: expectedResult,
});
});
});
6 changes: 3 additions & 3 deletions src/pages/departures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ function DeparturesContent({ autocompleteFeatures }: DeparturesContentProps) {
);
}

type DeparturesProps = WithGlobalData<DeparturesContentProps>;
const Departures: NextPage<DeparturesProps> = (props) => {
export type DeparturesPageProps = WithGlobalData<DeparturesContentProps>;
const DeparturesPage: NextPage<DeparturesPageProps> = (props) => {
return (
<DefaultLayout {...props}>
<DeparturesContent {...props} />
</DefaultLayout>
);
};

export default Departures;
export default DeparturesPage;

export const getServerSideProps = withGlobalData<DeparturesContentProps>(
withDepartureClient(async function ({ client }) {
Expand Down
3 changes: 3 additions & 0 deletions src/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { vi } from 'vitest';

vi.stubEnv('NEXT_PUBLIC_WEBSHOP_ORG_ID', 'atb');
31 changes: 31 additions & 0 deletions src/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { HttpClient } from '@atb/modules/api-server';
import { HttpEndpoints } from '@atb/modules/api-server/utils';
import type {
GetServerSideProps,
GetServerSidePropsContext,
GetServerSidePropsResult,
} from 'next/types';
import { Assertion, expect } from 'vitest';

export async function expectProps<T>(
potential: GetServerSidePropsResult<T>,
): Promise<Assertion<Awaited<T>>> {
if (!('props' in potential)) {
expect.unreachable();
}

const props = await potential.props;
return expect(props);
}

export async function getServerSidePropsWithClient<
U extends HttpEndpoints,
M,
T extends { [key: string]: any } = { [key: string]: any },
>(
client: HttpClient<U, M>,
propsHandler: GetServerSideProps<T>,
context?: GetServerSidePropsContext<T>,
) {
return propsHandler({ ...context, client } as GetServerSidePropsContext<T>);
}
18 changes: 18 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
plugins: [react()],

test: {
setupFiles: './src/tests/setup.ts',
environment: 'happy-dom',
},
resolve: {
alias: {
'@atb': path.resolve(__dirname, './src'),
},
},
});
Loading

0 comments on commit 1c03c6e

Please sign in to comment.