Home > OS >  Why my unit tests are complaining about unhandled promise rejection?
Why my unit tests are complaining about unhandled promise rejection?

Time:12-15

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?

CodePudding user response:

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);
});

CodePudding user response:

This is caused by a combination of using an async function for the executor argument to new Promise and not returning early after the reject() call. Without either of these, you wouldn't both reject() the promise and also cause the failure to reject the promise returned by the async function. Fixing both, you'd write

async function generateOnePDF(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 logo = await fetchImage(`${url}/static/media/logo_157.c15ac239.svg`);
    doc.addSVG(logo.toString(), 32, 40, {});

    …

    return new Promise((resolve, reject) => {
        const stream = doc.pipe(new Base64Encode());

        let finalString = '';
        stream.on('data', chunk => {
          finalString  = chunk;
        });
        stream.on('end', () => {
          resolve(finalString);
        });
        stream.on('error', reject);
        doc.end();
    });
});
  • Related