From af40ff60ff594d91751fc9f84e9ffc3fb6c99a9f Mon Sep 17 00:00:00 2001 From: gkuzin13 Date: Tue, 2 Jan 2024 14:55:21 +0200 Subject: [PATCH] chore: implement canvas share tests --- apps/client/package.json | 1 + apps/client/src/__tests__/App.test.tsx | 44 +++++++++ .../src/components/Elements/Loader/Loader.tsx | 2 +- .../SharePanel/__tests__/SharePanel.test.tsx | 25 +++++ apps/client/src/components/QRCode/QRCode.tsx | 7 +- apps/client/src/test/browser-mocks.ts | 91 ++++++++++++++----- apps/client/src/test/mocks/handlers.ts | 45 +++++++++ apps/client/src/test/mocks/server.ts | 4 + apps/client/src/test/setup.ts | 39 ++------ apps/client/src/test/test-utils.tsx | 22 +++-- pnpm-lock.yaml | 3 + 11 files changed, 217 insertions(+), 66 deletions(-) create mode 100644 apps/client/src/components/Panels/SharePanel/__tests__/SharePanel.test.tsx create mode 100644 apps/client/src/test/mocks/handlers.ts create mode 100644 apps/client/src/test/mocks/server.ts diff --git a/apps/client/package.json b/apps/client/package.json index 75084f15..d4ad6bb5 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -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" diff --git a/apps/client/src/__tests__/App.test.tsx b/apps/client/src/__tests__/App.test.tsx index 12da856d..0f9d84b8 100644 --- a/apps/client/src/__tests__/App.test.tsx +++ b/apps/client/src/__tests__/App.test.tsx @@ -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(); 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(); + + 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(, { + 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(); + }); + }); }); diff --git a/apps/client/src/components/Elements/Loader/Loader.tsx b/apps/client/src/components/Elements/Loader/Loader.tsx index 16c4ab08..e86a582e 100644 --- a/apps/client/src/components/Elements/Loader/Loader.tsx +++ b/apps/client/src/components/Elements/Loader/Loader.tsx @@ -5,7 +5,7 @@ type Props = PropsWithChildren<(typeof Styled.Container)['defaultProps']>; const Loader = ({ children, ...restProps }: Props) => { return ( - + {children} diff --git a/apps/client/src/components/Panels/SharePanel/__tests__/SharePanel.test.tsx b/apps/client/src/components/Panels/SharePanel/__tests__/SharePanel.test.tsx new file mode 100644 index 00000000..e81ebcdf --- /dev/null +++ b/apps/client/src/components/Panels/SharePanel/__tests__/SharePanel.test.tsx @@ -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(); + + // 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(); + + // open share panel + await user.click(screen.getByText(/Share/i)); + + await waitFor(() => { + expect(screen.getByTestId(/qr-code/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/apps/client/src/components/QRCode/QRCode.tsx b/apps/client/src/components/QRCode/QRCode.tsx index 7337373b..249dfe15 100644 --- a/apps/client/src/components/QRCode/QRCode.tsx +++ b/apps/client/src/components/QRCode/QRCode.tsx @@ -5,7 +5,12 @@ type Props = { }; const QRCode = ({ dataUrl }: Props) => { - return ; + return ( + + ); }; export default QRCode; diff --git a/apps/client/src/test/browser-mocks.ts b/apps/client/src/test/browser-mocks.ts index 3479cdbb..6d7ce9ac 100644 --- a/apps/client/src/test/browser-mocks.ts +++ b/apps/client/src/test/browser-mocks.ts @@ -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 = {}; - - 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 = {}; + + 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, + }, + }); +} diff --git a/apps/client/src/test/mocks/handlers.ts b/apps/client/src/test/mocks/handlers.ts new file mode 100644 index 00000000..7ea25d2a --- /dev/null +++ b/apps/client/src/test/mocks/handlers.ts @@ -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); + }), +]; diff --git a/apps/client/src/test/mocks/server.ts b/apps/client/src/test/mocks/server.ts new file mode 100644 index 00000000..e52fee0a --- /dev/null +++ b/apps/client/src/test/mocks/server.ts @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; + +export const server = setupServer(...handlers); diff --git a/apps/client/src/test/setup.ts b/apps/client/src/test/setup.ts index 41c0057b..0c0994b6 100644 --- a/apps/client/src/test/setup.ts +++ b/apps/client/src/test/setup.ts @@ -1,15 +1,20 @@ 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 @@ -17,33 +22,3 @@ afterEach(() => { // 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); diff --git a/apps/client/src/test/test-utils.tsx b/apps/client/src/test/test-utils.tsx index 527c5a89..b7637589 100644 --- a/apps/client/src/test/test-utils.tsx +++ b/apps/client/src/test/test-utils.tsx @@ -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'; @@ -70,16 +72,18 @@ export function renderWithProviders( function Wrapper({ children, }: PropsWithChildren<{ children: React.ReactNode }>) { + const roomId = urlSearchParam.get(PAGE_URL_SEARCH_PARAM_KEY); + return ( - - - - - {children} - - - - + + + + + {children} + + + + ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8d68a95..6e21147d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,6 +187,9 @@ importers: jsdom: specifier: ^22.1.0 version: 22.1.0(canvas@2.11.2) + msw: + specifier: '*' + version: 2.0.11(typescript@4.9.5) resize-observer-polyfill: specifier: ^1.5.1 version: 1.5.1