Search code examples
javascriptexceptionpromisees6-promise

Why my unit tests are complaining about unhandled promise rejection?


I have the below function to print receipts:

const generateOnePDF = async (ticket, orderData) =>
  new Promise(async (resolve, reject) => {
    let finalString = '';

    if (!ticket) {
      const err = new Error('missing ticket data');
      reject(err);
    }
    if (!orderData) {
      const err = new Error('missing order data');
      reject(err);
    }

    const { payload, sequence, title } = ticket;

    if (!payload) {
      const err = new Error('missing payload data');
      reject(err);
    }
    if (!sequence) {
      const err = new Error('missing ticket sequence');
      reject(err);
    }
    if (!title) {
      const err = new Error('missing ticket book title');
      reject(err);
    }

    const doc = new PDFDocument();
    PDFDocument.prototype.addSVG = function (svg, x, y, options) {
      return SVGtoPDF(this, svg, x, y, options);
    };

    const stream = doc.pipe(new Base64Encode());

    // logo
    const logo = await fetchImage(
      `${url}/static/media/logo_157.c15ac239.svg`
    );
    doc.addSVG(logo.toString(), 32, 40, {});

    doc.moveUp();
    doc.moveUp();
    doc.text(`Order: O${orderData.orderId}`, { align: 'right' });

    const rectXOffset = 25;
    const rectPosition = 32;
    doc
      .rect(rectXOffset, rectPosition, doc.page.width - rectXOffset * 2, 32)
      .stroke();

    doc.moveDown();
    doc.moveDown();
    doc.moveDown();

    doc.rect(rectXOffset, 80, doc.page.width - rectXOffset * 2, 680).stroke();

    doc.text(orderData.title, { align: 'left' });

    doc.moveDown();

    doc.text(orderData.venue, { align: 'left' });
    doc.text(orderData.address, { align: 'left' });
    doc.text(`${orderData.city}, ${orderData.province} ${orderData.zip}`, {
      align: 'left'
    });

    if (orderData.custom) {
      doc.moveDown();
      doc.text(orderData.customCopy, { align: 'left' });
    }

    doc.moveDown();
    doc.text(`${orderData.date} at ${orderData.show}`, { align: 'left' });

    doc.moveDown();
    const image = await fetchImage(orderData['image']);
    doc.image(image, {
      fit: [100, 100],
      align: 'right',
      valign: 'top'
    });

    doc.moveDown();
    doc.text(
      `Order: O${orderData.orderId}. Placed by ${orderData.firstName} ${orderData.lastName} on ${orderData.created}`
    );


    // right column
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();
    doc.moveUp();

    doc.text(`Ticket: ${sequence} ${title}`, { align: 'right' });

    const qrcode = new QRCode({
      content: payload,
      width: 200,
      height: 200,
      xmlDeclaration: false,
      join: true
    }).svg();

    const options = { align: 'right' };
    doc.addSVG(qrcode, 400, 150, options);

    // finalize/close the PDF file
    doc.end();

    stream.on('data', (chunk) => {
      finalString += chunk;
    });

    stream.on('end', () => {
      // the stream is at its end, so push the resulting base64 string to the response
      resolve(finalString);
    });

    stream.on('error', (err) => {
      reject(err);
    });
  });

It is not the cleanest piece of code in the world, but it works for me, in the meantime.

I added a simple unit test for this code, below:

  it('should throw an error if the ticket or order data are invalid', async () => {
    await expect(generateOnePDF(null, {})).rejects.toThrowError();
    await expect(generateOnePDF({}, null)).rejects.toThrowError();
  });

The tests passes, but it writes "garbage" to the console. There are unhandled rejects in the code.

(node:53703) UnhandledPromiseRejectionWarning: TypeError: Cannot destructure property 'payload' of '((cov_2gbfm4phuo(...).s[25]++) , ticket)' as it is null.
(Use `node --trace-warnings ...` to show where the warning was created)

is one such error and another is:

(node:53703) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'orderId' of null

What I don't understand why if (!ticket) throw() and if (!orderData) throw would not prevent the errors from occurring. Where is the "broken promise?"

I am doing reject inside the promise handler function, so where I am not doing it?


Solution

  • The problem is that your function still continues executing after reject is called, and so errors occur.

    Moreover, you should not create a new Promise with an async callback. That is an anti-pattern. Instead promisify the stream "end" event, and then return that promise.

    I would also refrain from defining a function on that PDFDocument prototype, and certainly not on every call of generateOnePDF. You might as well just call that SVGtoPDF function directly.

    So something like this:

    const streamPromise = stream =>
        return new Promise((resolve, reject) => {
            let finalString = '';
            stream.on('data',  chunk => finalString += chunk);
            stream.on('end',   () => resolve(finalString));
            stream.on('error', reject);
        });
    
    const generateOnePDF = async (ticket, orderData) => {
        if (!ticket   ) throw new Error('missing ticket data');
        if (!orderData) throw new Error('missing order data');
        const { payload, sequence, title } = ticket;
        if (!payload  ) throw new Error('missing payload data');
        if (!sequence ) throw new Error('missing ticket sequence');
        if (!title    ) throw new Error('missing ticket book title');
        const doc = new PDFDocument();
        const stream = doc.pipe(new Base64Encode());
        // logo
        const logo = await fetchImage(`${url}/static/media/logo_157.c15ac239.svg`);
        SVGtoPDF(doc, logo.toString(), 32, 40, {});
        // ... etc ...
        // ...
        doc.end();
        return streamPromise(stream);
    });