Skip to content

Commit

Permalink
test uploaded presentation + session id for bc id
Browse files Browse the repository at this point in the history
  • Loading branch information
codyzu committed Jul 24, 2023
1 parent 1efca66 commit 638b84c
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 89 deletions.
20 changes: 14 additions & 6 deletions src/components/broadcast/use-broadcast-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@ import {nanoid} from 'nanoid';
import {type Handler, type Payload} from './use-channel-handlers';

export default function useBroadcastChannel({
channelId,
sessionId,
onIncoming,
}: {
channelId: string;
sessionId: string;
onIncoming?: Handler;
}): Handler {
const [postMessage, setPostMessage] = useState<() => void>(
() => () => undefined,
);

useEffect(() => {
const channel = new BroadcastChannel(channelId);
if (!sessionId) {
return;
}

const channel = new BroadcastChannel(sessionId);
let channelOpen = true;
const channelMessageHandler = (event: MessageEvent) => {
console.log('incoming bc', channelId, event);
if (!channelOpen) {
return;
}

console.log('incoming bc', sessionId, event);
if (onIncoming) {
const incomingPayload = event.data as Payload;

Expand All @@ -38,7 +46,7 @@ export default function useBroadcastChannel({

channel.addEventListener('message', channelMessageHandler);
setPostMessage(() => (payload: any) => {
console.log('posting bc data', channelId, payload);
console.log('posting bc data', sessionId, payload);
if (channelOpen) {
channel.postMessage(payload);
}
Expand All @@ -49,7 +57,7 @@ export default function useBroadcastChannel({
channel.removeEventListener('message', channelMessageHandler);
channel.close();
};
}, [onIncoming, channelId]);
}, [onIncoming, sessionId]);

return postMessage;
}
2 changes: 0 additions & 2 deletions src/components/pdf/Pdf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ function Pdf({
file={file}
className="w-full aspect-video relative rounded-t-md"
onLoadSuccess={(pdf) => {
console.log('LOADED!!!');
onSetPageCount(pdf.numPages);
}}
>
Expand All @@ -32,7 +31,6 @@ function Pdf({
// Fix the ratio to 1
devicePixelRatio={1}
onRenderSuccess={() => {
console.log('RENDERED!!!');
onPageRendered(canvasRef.current!);
}}
/>
Expand Down
27 changes: 16 additions & 11 deletions src/components/pdf/__mocks__/Pdf.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {readFile} from 'node:fs/promises';
import {vi} from 'vitest';
import {useCallback, useMemo} from 'react';
import OriginalPdf from '../Pdf';
import {type MagicFile} from '../../../test/magic-file';

Expand All @@ -20,23 +21,27 @@ export default function Pdf({
onPageRendered: (canvas: HTMLCanvasElement) => void;
pageIndex: number;
}) {
const dataUri = useMemo(() => (file as MagicFile).getDataUri(), [file]);
const onPageRenderedMockedToBlob = useCallback(
(canvas: HTMLCanvasElement) => {
// The mocked canvas toBlob doesn't do anything by default
// In our tests, we will call the callback with pre-rendered page image
vi.mocked(canvas.toBlob).mockImplementation((callback: BlobCallback) => {
callback(pages[pageIndex] as unknown as Blob);
});
onPageRendered(canvas);
},
[onPageRendered, pageIndex],
);

return (
<OriginalPdf
// Rendering with jsdom seems to only work when using a data URI
// Convert the uploaded file to a data URI for the test
file={(file as MagicFile).getDataUri()}
file={dataUri}
pageIndex={pageIndex}
onSetPageCount={onSetPageCount}
onPageRendered={(canvas) => {
// The mocked canvas toBlob doesn't do anything by default
// In our tests, we will call the callback with pre-rendered page image
vi.mocked(canvas.toBlob).mockImplementation(
(callback: BlobCallback) => {
callback(pages[pageIndex] as unknown as Blob);
},
);
onPageRendered(canvas);
}}
onPageRendered={onPageRenderedMockedToBlob}
/>
);
}
3 changes: 1 addition & 2 deletions src/pages/Presentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ function Presentation() {
}`;
}, [presentation]);

// Const [forward, setForward] = useState<boolean>(true);
const sessionId = useSearchParametersSessionId(true);

// Sync the slide index with the broadcast channel (speaker view)
Expand All @@ -40,7 +39,7 @@ function Presentation() {
setHandlers: setHandlersBroadcastChannel,
} = useChannelHandlers();
const postBroadcastChannel = useBroadcastChannel({
channelId: presentation.id ?? 'unknown',
sessionId,
onIncoming: handleIncomingBroadcastChannel,
});
const {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Speaker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function Speaker() {
setHandlers: setHandlersBroadcastChannel,
} = useChannelHandlers();
const postBroadcastChannel = useBroadcastChannel({
channelId: presentation.id ?? 'unknown',
sessionId,
onIncoming: handleIncomingBroadcastChannel,
});
const {
Expand Down
91 changes: 54 additions & 37 deletions src/pages/Upload.Test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import {readFile} from 'node:fs/promises';
import {type UserCredential, signInAnonymously} from 'firebase/auth';
import {type PropsWithChildren} from 'react';
import {Suspense, type PropsWithChildren} from 'react';
import {vi} from 'vitest';
import {collection, deleteDoc, getDocs, query, where} from 'firebase/firestore';
import {
type FirebaseStorage,
deleteObject,
getStorage,
ref,
} from 'firebase/storage';
import {collection, getDocs, query, where} from 'firebase/firestore';
import {type FirebaseStorage, getStorage} from 'firebase/storage';
import {act} from 'react-dom/test-utils';
import {app, auth, firestore} from '../firebase';
import {screen, userEvent, renderRoute, waitFor} from '../test/test-utils';
import {screen, userEvent, renderRoute} from '../test/test-utils';
import {UserContext} from '../components/UserProvider';
import {MagicFile} from '../test/magic-file';
// eslint-disable-next-line import/no-unassigned-import
Expand All @@ -25,37 +21,21 @@ let storage: FirebaseStorage;
beforeAll(async () => {
cred = await signInAnonymously(auth);
storage = getStorage(app);
// Const admin = initializeApp({projectId: 'demo-test'});
// const dbAdmin = getFirestore(admin);

// const existing = await dbAdmin
// .collection('presentations')
// .where('uid', '==', cred.user.uid)
// .get();

const existing = await getDocs(
query(
collection(firestore, 'presentations'),
where('uid', '==', cred.user.uid),
),
);

// Remove any existing presentations
await Promise.all(
existing.docs.map(async (snapshot) => {
await deleteObject(ref(storage, `presentations/${snapshot.id}`));
await deleteDoc(snapshot.ref);
}),
);
});

describe('Presentation view', () => {
let pdfFile: MagicFile;
it('renders and uploads the test presentation', async () => {
const userContext = {
user: {email: cred.user.email ?? undefined, uid: cred.user.uid},
setUser: () => undefined,
};

vi.spyOn(window, 'getComputedStyle').mockImplementation(() => {
const styles = new CSSStyleDeclaration();
return styles;
});

renderRoute('/upload', {
wrapper: ({children}: PropsWithChildren) => (
<UserContext.Provider value={userContext}>
Expand All @@ -78,7 +58,7 @@ describe('Presentation view', () => {

// The magic file can be transformed to dataURI for react-pdf.Document
// and a buffer for firebase/storage.uploadBytes (which does not support File objects when running in node)
const pdfFile = new MagicFile(
pdfFile = new MagicFile(
await readFile('./src/test/pdf/test.pdf'),
'test.pdf',
{
Expand All @@ -87,12 +67,49 @@ describe('Presentation view', () => {
);

// Jsdom mocks getComputedStyle, we need to return something so that the watermarking works
vi.spyOn(window, 'getComputedStyle').mockImplementation(() => {
const styles = new CSSStyleDeclaration();
return styles;
await userEvent.upload(uploaderInput!, pdfFile);
await screen.findByText(/done/i);
});

it('renders the uploaded presentation', async () => {
const existing = await getDocs(
query(
collection(firestore, 'presentations'),
where('uid', '==', cred.user.uid),
),
);
expect(existing.docs).toHaveLength(1);
const presentationId = existing.docs[0].id;

// RenderRoute(`/p/${presentationId}`);
renderRoute(`/p/${presentationId}`, {
wrapper: ({children}: PropsWithChildren) => (
// Not sure why, but need suspense here after the previous test
<Suspense>{children}</Suspense>
),
});

await userEvent.upload(uploaderInput!, pdfFile);
await waitFor(() => screen.getByText(/done/i));
const slide1ImageElement = await screen.findByRole('img', {
name: 'Slide page 1',
});
expect(slide1ImageElement).toBeInTheDocument();

const slide1ImageUrl = slide1ImageElement.getAttribute('src');
expect(slide1ImageUrl).not.toBeNull();

let slide1ImageActual;
let slide1ImageExpected;

// Things can shift around (the toolbar becomes hidden specifically) while loading the image data
// Run them inside act to avoid errors
await act(async () => {
const slide1ImageResponse = await fetch(slide1ImageUrl!);
const slide1ImageBuffer = await slide1ImageResponse.arrayBuffer();
const slide1ImageFile = await readFile('./src/test/pdf/page1.webp');
slide1ImageActual = new Uint8Array(slide1ImageBuffer);
slide1ImageExpected = new Uint8Array(slide1ImageFile);
});

expect(slide1ImageActual).toEqual(slide1ImageExpected);
});
});
63 changes: 33 additions & 30 deletions src/pages/Upload.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {useCallback, useContext, useEffect, useRef, useState} from 'react';
import {useCallback, useContext, useEffect, useState} from 'react';
import {useDropzone} from 'react-dropzone';
import {Document, Page} from 'react-pdf';
import * as pdfjs from 'pdfjs-dist';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
Expand Down Expand Up @@ -82,8 +81,21 @@ export default function Upload() {
const [title, setTitle] = useState('');
const [savingState, setSavingState] = useState<NotesSaveState>('saved');

const onDrop = useCallback(
async (acceptedFiles: File[]) => {
const {getRootProps, getInputProps, isDragActive, acceptedFiles} =
useDropzone({
accept: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'application/pdf': ['.pdf'],
},
maxFiles: 1,
});

useEffect(() => {
async function startRenderingFile() {
if (!acceptedFiles[0]) {
return;
}

setFile(acceptedFiles[0]);

const presentationRef = await addDoc(
Expand Down Expand Up @@ -114,22 +126,15 @@ export default function Upload() {
original: originalDownloadUrl,
} satisfies PresentationUpdate);
setRendering(true);
},
[userData?.username, userData?.twitterHandle],
);
}

const {getRootProps, getInputProps, isDragActive} = useDropzone({
onDrop,
accept: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'application/pdf': ['.pdf'],
},
maxFiles: 1,
});
void startRenderingFile();
}, [acceptedFiles, userData?.twitterHandle, userData?.username]);

const [renderingDone, setRenderingDone] = useState(false);
const [pageBlob, setPageBlob] = useState<Blob>();
function pageRendered(canvas: HTMLCanvasElement) {

const pageRendered = useCallback((canvas: HTMLCanvasElement) => {
// Watermark rendered image
const ctx = canvas.getContext('2d');
if (ctx) {
Expand Down Expand Up @@ -164,12 +169,12 @@ export default function Upload() {

canvas.toBlob(async (blob) => {
if (!blob) {
throw new Error(`Error rendering page index ${pageIndex}`);
throw new Error(`Error rendering pdf canvas to image webp blob`);
}

setPageBlob(blob);
}, 'image/webp');
}
}, []);

const [uploadPromises, setUploadPromises] = useState<Array<Promise<string>>>(
[],
Expand Down Expand Up @@ -223,15 +228,15 @@ export default function Upload() {
}));

setSavingState('saving');
setPages(nextPages);
setNotes(nextNotes);

await updateDoc(presentationRef, {
pages: nextPages,
rendered: new Date(),
title,
notes: nextNotes,
} satisfies PresentationUpdate);
setPages(nextPages);
setNotes(nextNotes);
setUploadDone(true);
setSavingState((currentState) =>
currentState === 'saving' ? 'saved' : currentState,
Expand Down Expand Up @@ -329,16 +334,14 @@ export default function Upload() {
)}
{file && (
<div className="relative flex flex-col w-full max-w-screen-sm">
<Pdf
pageIndex={pageIndex}
file={file}
onSetPageCount={(count) => {
setPageCount(count);
}}
onPageRendered={(canvas) => {
pageRendered(canvas);
}}
/>
{!renderingDone && (
<Pdf
pageIndex={pageIndex}
file={file}
onSetPageCount={setPageCount}
onPageRendered={pageRendered}
/>
)}
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center bg-teal-800 bg-opacity-90">
<div className={clsx('w-10 h-10', icon)} />
<div>{message}</div>
Expand Down

0 comments on commit 638b84c

Please sign in to comment.