Skip to content

Commit

Permalink
chore: implement canvas share tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gkuzin13 committed Jan 2, 2024
1 parent 94ae101 commit af40ff6
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 66 deletions.
1 change: 1 addition & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"eslint-plugin-playwright": "^0.15.3",
"happy-dom": "^10.9.0",
"jsdom": "^22.1.0",
"msw": "*",
"resize-observer-polyfill": "^1.5.1",
"vite-plugin-pwa": "^0.16.4",
"vitest-canvas-mock": "^0.3.2"
Expand Down
44 changes: 44 additions & 0 deletions apps/client/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,54 @@
import { renderWithProviders } from '@/test/test-utils';
import App from '@/App';
import { mockGetPageResponse } from '@/test/mocks/handlers';
import { screen, waitFor } from '@testing-library/react';
import { PAGE_URL_SEARCH_PARAM_KEY } from '@/constants/app';
import { setSearchParam } from '@/test/browser-mocks';
import { nodesGenerator, stateGenerator } from '@/test/data-generators';

describe('App', () => {
afterEach(() => {
Object.defineProperty(window, 'location', window.location);
});

it('mounts without crashing', () => {
const { container } = renderWithProviders(<App />);

expect(container).toBeInTheDocument();
});

it('sets canvas state from fetched data when in collab mode', async () => {
setSearchParam(PAGE_URL_SEARCH_PARAM_KEY, mockGetPageResponse.page.id);

const { store } = renderWithProviders(<App />);

await waitFor(() => {
const { nodes } = store.getState().canvas.present;

expect(nodes).toEqual(mockGetPageResponse.page.nodes);
});
});

it('calls share this page', async () => {
Object.defineProperty(window.location, 'reload', {
value: vi.fn(),
});

const { user } = renderWithProviders(<App />, {
preloadedState: stateGenerator({
canvas: {
present: {
nodes: nodesGenerator(6),
},
},
}),
});

await user.click(screen.getByText(/Share/i));
await user.click(screen.getByText(/Share this page/i));

await waitFor(() => {
expect(screen.getByTestId(/loader/i)).toBeInTheDocument();
});
});
});
2 changes: 1 addition & 1 deletion apps/client/src/components/Elements/Loader/Loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type Props = PropsWithChildren<(typeof Styled.Container)['defaultProps']>;

const Loader = ({ children, ...restProps }: Props) => {
return (
<Styled.Container {...restProps}>
<Styled.Container {...restProps} data-testid="loader">
<Styled.InnerContainer>
<Styled.Spinner name="spinner" size="md" />
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { screen, waitFor } from '@testing-library/react';
import { renderWithProviders } from '@/test/test-utils';
import SharePanel from '../SharePanel';

describe('SharePanel', () => {
it('displays shareable content if canvas is not shared', async () => {
const { user } = renderWithProviders(<SharePanel isPageShared={false} />);

// open share panel
await user.click(screen.getByText(/Share/i));

expect(screen.getByText(/Share this page/i)).toBeInTheDocument();
});

it('displays qrcode if the canvas is shared', async () => {
const { user } = renderWithProviders(<SharePanel isPageShared />);

// open share panel
await user.click(screen.getByText(/Share/i));

await waitFor(() => {
expect(screen.getByTestId(/qr-code/i)).toBeInTheDocument();
});
});
});
7 changes: 6 additions & 1 deletion apps/client/src/components/QRCode/QRCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ type Props = {
};

const QRCode = ({ dataUrl }: Props) => {
return <Styled.Background style={{ backgroundImage: `url(${dataUrl})` }} />;
return (
<Styled.Background
style={{ backgroundImage: `url(${dataUrl})` }}
data-testid="qr-code"
/>
);
};

export default QRCode;
91 changes: 68 additions & 23 deletions apps/client/src/test/browser-mocks.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,68 @@
export const matchMedia = vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));

export const localStorage = vi.fn(() => {
let storage: Record<string, string> = {};

return {
getItem: (key: string) => storage[key],
setItem: (key: string, value: string) => (storage[key] = value),
clear: () => (storage = {}),
removeItem: (key: string) => delete storage[key],
length: Object.keys(storage).length,
key: (index: number) => Object.keys(storage)[index] || null,
};
})();
import { urlSearchParam } from '@/utils/url';
import ResizeObserverPolyfill from 'resize-observer-polyfill';

// matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});

// localStorage
Object.defineProperty(window, 'localStorage', {
value: vi.fn().mockImplementation(() => {
let storage: Record<string, string> = {};

return {
getItem: (key: string) => storage[key],
setItem: (key: string, value: string) => (storage[key] = value),
clear: () => (storage = {}),
removeItem: (key: string) => delete storage[key],
length: Object.keys(storage).length,
key: (index: number) => Object.keys(storage)[index] || null,
};
})(),
});

/**
* implement missing DragEvent in jsdom
* https://github.com/jsdom/jsdom/issues/2913
*/
export class DragEvent extends MouseEvent {
public clientX: number;
public clientY: number;

constructor(type: string, params: PointerEventInit = {}) {
super(type, params);
this.clientX = params.clientX ?? 0;
this.clientY = params.clientY ?? 0;
}
}

global.DragEvent =
global.DragEvent ?? (DragEvent as typeof globalThis.PointerEvent);

// fonts load
Object.defineProperty(document, 'fonts', {
value: { ready: Promise.resolve({}) },
configurable: true,
});

// resizeObserver
global.ResizeObserver = ResizeObserverPolyfill;

export function setSearchParam(key: string, value: string) {
Object.defineProperty(window, 'location', {
value: {
search: urlSearchParam.set(key, value).search,
},
});
}
45 changes: 45 additions & 0 deletions apps/client/src/test/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { HttpResponse, http, delay } from 'msw';
import { v4 as uuid } from 'uuid';
import { nodesGenerator, stateGenerator } from '../data-generators';
import { BASE_URL_DEV } from '@/constants/app';
import type {
GetPageResponse,
QRCodeResponse,
SharePageResponse,
} from 'shared';

const mockPageId = uuid();

export const mockGetPageResponse: GetPageResponse = {
page: {
nodes: nodesGenerator(5),
stageConfig: stateGenerator({}).canvas.present.stageConfig,
id: mockPageId,
},
};

export const mockSharePageResponse: SharePageResponse = {
id: mockPageId,
};

export const mockQRCodeResponse: QRCodeResponse = {
dataUrl: 'data:image/png',
};

export const handlers = [
http.get(`${BASE_URL_DEV}/p/*`, async () => {
await delay(150);

return HttpResponse.json(mockGetPageResponse);
}),
http.post(`${BASE_URL_DEV}/p`, async () => {
await delay(150);

return HttpResponse.json(mockSharePageResponse);
}),
http.post(`${BASE_URL_DEV}/qrcode`, async () => {
await delay(150);

return HttpResponse.json(mockQRCodeResponse);
}),
];
4 changes: 4 additions & 0 deletions apps/client/src/test/mocks/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
39 changes: 7 additions & 32 deletions apps/client/src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,24 @@
import * as matchers from '@testing-library/jest-dom/matchers';
import { cleanup } from '@testing-library/react';
import ResizeObserverPolyfill from 'resize-observer-polyfill';
import './browser-mocks';
import 'vitest-canvas-mock';
import { localStorage, matchMedia } from './browser-mocks';
import { server } from './mocks/server';

expect.extend(matchers);

beforeAll(() => server.listen());

afterEach(() => {
cleanup();
server.resetHandlers();
});

afterAll(() => server.close());

/**
* temporary fix for tests that use jest-canvas-mock
* otherwise throws error
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
globalThis.jest = vi;

global.ResizeObserver = ResizeObserverPolyfill;
global.matchMedia = matchMedia;
global.localStorage = localStorage;

/**
* mock document.fonts load
*/
Object.defineProperty(document, 'fonts', {
value: { ready: Promise.resolve({}) },
configurable: true,
});

/**
* implement missing DragEvent in jsdom
* https://github.com/jsdom/jsdom/issues/2913
*/
class DragEvent extends MouseEvent {
public clientX: number;
public clientY: number;

constructor(type: string, params: PointerEventInit = {}) {
super(type, params);
this.clientX = params.clientX ?? 0;
this.clientY = params.clientY ?? 0;
}
}

global.DragEvent =
global.DragEvent ?? (DragEvent as typeof globalThis.PointerEvent);
22 changes: 13 additions & 9 deletions apps/client/src/test/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { WebSocketProvider } from '@/contexts/websocket';
import { ThemeProvider } from '@/contexts/theme';
import { NotificationsProvider } from '@/contexts/notifications';
import { ModalProvider } from '@/contexts/modal';
import { PAGE_URL_SEARCH_PARAM_KEY } from '@/constants/app';
import { urlSearchParam } from '@/utils/url';
import type { PropsWithChildren } from 'react';
import type { PreloadedState } from '@reduxjs/toolkit';
import type { RootState } from '@/stores/store';
Expand Down Expand Up @@ -70,16 +72,18 @@ export function renderWithProviders(
function Wrapper({
children,
}: PropsWithChildren<{ children: React.ReactNode }>) {
const roomId = urlSearchParam.get(PAGE_URL_SEARCH_PARAM_KEY);

return (
<ThemeProvider>
<StoreProvider store={store}>
<ModalProvider>
<NotificationsProvider>
<WebSocketProvider roomId={null}>{children}</WebSocketProvider>
</NotificationsProvider>
</ModalProvider>
</StoreProvider>
</ThemeProvider>
<StoreProvider store={store}>
<WebSocketProvider roomId={roomId}>
<ThemeProvider>
<ModalProvider>
<NotificationsProvider>{children}</NotificationsProvider>
</ModalProvider>
</ThemeProvider>
</WebSocketProvider>
</StoreProvider>
);
}

Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit af40ff6

Please sign in to comment.