UNPKG

apostrophe

Version:
559 lines (497 loc) • 15.5 kB
const t = require('../test-lib/test.js'); const assert = require('assert/strict'); const fs = require('fs'); const fsp = require('fs/promises'); const path = require('path'); const FormData = require('form-data'); const publicFolderPath = path.join(process.cwd(), 'test/public'); describe('Images', function() { let apos; let jar; let inserted; let image; const mockImages = [ { type: '@apostrophecms/image', slug: 'image-1', visibility: 'public', attachment: { extension: 'jpg', width: 500, height: 400 } }, { type: '@apostrophecms/image', slug: 'image-2', visibility: 'public', attachment: { extension: 'jpg', width: 500, height: 400 } }, { type: '@apostrophecms/image', slug: 'image-3', visibility: 'public', attachment: { extension: 'jpg', width: 150, height: 150 } }, { type: '@apostrophecms/image', slug: 'image-4', visibility: 'public', attachment: { extension: 'svg' } } ]; this.timeout(t.timeout); after(function() { return t.destroy(apos); }); it('should be a property of the apos object', async function() { this.timeout(t.timeout); this.slow(2000); apos = await t.create({ root: module }); assert(apos.image); assert(apos.image.__meta.name === '@apostrophecms/image'); }); // Test pieces.list() it('should clean up any existing images for testing', async function() { try { const response = await apos.doc.db.deleteMany( { type: '@apostrophecms/image' } ); assert(response.result.ok === 1); } catch (e) { assert(false); } }); it('should add images for testing', async function() { assert(apos.image.insert); const req = apos.task.getReq(); const insertPromises = mockImages.map(async (image) => { return apos.image.insert(req, image); }); inserted = await Promise.all(insertPromises); assert(inserted.length === mockImages.length); assert(inserted[0]._id); }); it('should respect minSize filter (svg is always OK)', async function() { const req = apos.task.getAnonReq(); const images = await apos.image.find(req).minSize([ 200, 200 ]).toArray(); assert(images.length === 3); }); it('should respect minSize filter in toCount, which uses a cloned cursor', async function() { const req = apos.task.getAnonReq(); const count = await apos.image.find(req).minSize([ 200, 200 ]).toCount(); assert(count === 3); }); it('should generate a srcset string for an image', function() { const srcset = apos.image.srcset({ name: 'test', _id: 'test', extension: 'jpg', width: 1200, height: 800 }); assert.strictEqual(srcset, [ '/uploads/attachments/test-test.max.jpg 1200w', '/uploads/attachments/test-test.full.jpg 1140w', '/uploads/attachments/test-test.two-thirds.jpg 760w', '/uploads/attachments/test-test.one-half.jpg 570w', '/uploads/attachments/test-test.one-third.jpg 380w', '/uploads/attachments/test-test.one-sixth.jpg 190w' ].join(', ')); }); it('should not generate a srcset string for an SVG image', function() { const srcset = apos.image.srcset({ name: 'test', _id: 'test', extension: 'svg', width: 1200, height: 800 }); assert.strictEqual(srcset, ''); }); it('should be able to insert test users', async function() { await insertUser({ title: 'admin', username: 'admin', password: 'admin', email: 'ad@min.com', role: 'admin' }); await insertUser({ title: 'contributor', username: 'contributor', password: 'contributor', email: 'con@tributor.com', role: 'contributor' }); }); it('REST: should be able to log in as admin', async function() { jar = await login('admin'); }); it('"editable" API includes images for admin', async function() { const editable = await getEditableImages(jar); assert(editable.length === 4); }); it('REST: should be able to log in as contributor', async function() { jar = await login('contributor'); }); it('"editable" API does not include images for contributor', async function() { const editable = await getEditableImages(jar); assert(editable.length === 0); }); it('REST: should be able to upload an image with an attachment as an admin', async function() { jar = await login('admin'); const formData = new FormData(); formData.append('file', fs.createReadStream(path.join(apos.rootDir, '/public/test-image.jpg'))); // Make an async request to upload the image. const attachment = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', { body: formData, jar }); image = await apos.http.post('/api/v1/@apostrophecms/image', { body: { title: 'Test Image', attachment }, jar }); assert(image); assert(image.title === 'Test Image'); }); it('REST: autocrop should have no effect when there are no widget options', async function() { const result = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', { body: { relationship: [ image ], widgetOptions: {} }, jar }); assert(result.relationship); assert(result.relationship[0]); assert(result.relationship[0].title === 'Test Image'); assert(!result.relationship[0]._fields); }); it('REST: autocrop should work when aspectRatio is less than actual image', async function() { const result = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', { body: { relationship: [ image ], widgetOptions: { aspectRatio: [ 1, 2 ] } }, jar }); assert(result.relationship); const output = result.relationship[0]; assert(output); assert(output.title === 'Test Image'); const fields = output._fields; assert(fields); // Useful for visual verification // require('child_process').execSync(`open // test/public${output.attachment._urls.full} &`); assert.strictEqual(fields.top, 0); assert.strictEqual(fields.left, 75); assert.strictEqual(fields.width, 300); assert.strictEqual(fields.height, 600); }); it('REST: autocrop should work when aspectRatio is greater than actual image', async function() { const result = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', { body: { relationship: [ image ], widgetOptions: { aspectRatio: [ 2, 1 ] } }, jar }); assert(result.relationship); const output = result.relationship[0]; assert(output); assert(output.title === 'Test Image'); const fields = output._fields; assert(fields); // Useful for visual verification // require('child_process').execSync(`open // test/public${output.attachment._urls.full} &`); assert.strictEqual(fields.top, 187); assert.strictEqual(fields.left, 0); assert.strictEqual(fields.width, 450); assert.strictEqual(fields.height, 225); }); it('should update crop fields when replacing an image attachment', async function () { await t.destroy(apos); await fsp.rm(path.join(publicFolderPath, 'uploads'), { recursive: true, force: true }); apos = await t.create({ root: module, modules: { 'test-piece': { extend: '@apostrophecms/piece-type', fields: { add: { main: { type: 'area', options: { widgets: { '@apostrophecms/image': { aspectRatio: [ 3, 2 ] } } } } } } } } }); await insertUser({ title: 'admin', username: 'admin', password: 'admin', email: 'ad@min.com', role: 'admin' }); // Upload an image (landscape), crop it, insert a piece with the cropped // image jar = await login('admin'); const formData = new FormData(); const stream = fs.createReadStream( path.join(apos.rootDir, '/public/test-image-landscape.jpg') ); formData.append('file', stream); const attachment = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', { body: formData, jar }); stream.close(); image = await apos.http.post('/api/v1/@apostrophecms/image', { body: { title: 'Test Image Landscape', attachment }, jar }); assert.equal(image._prevAttachmentId, attachment._id); const crop = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', { body: { relationship: [ image ], widgetOptions: { aspectRatio: [ 3, 2 ] } }, jar }); let piece = await apos.http.post('/api/v1/test-piece', { jar, body: { title: 'Test Piece', slug: 'test-piece', type: 'test-piece', main: { metaType: 'area', items: [ { type: '@apostrophecms/image', metaType: 'widget', imageIds: [ image.aposDocId ], imageFields: { [image.aposDocId]: crop.relationship[0]._fields }, _image: [ crop.relationship[0] ] } ] } } }); let imageFields = piece.main.items[0].imageFields[image.aposDocId]; assert(imageFields, 'imageFields should be present when creating the piece'); assert.equal(imageFields.width / imageFields.height, 3 / 2, 'aspect ratio should be 3:2'); await fsp.access( path.join( publicFolderPath, attachment._urls.original.replace( '.jpg', `.${imageFields.left}.${imageFields.top}.${imageFields.width}.${imageFields.height}.jpg` ) ) ); // Replace the image with portrait orientation, verify that the aspect // ratio is preserved const formDataPortrait = new FormData(); const streamPortrait = fs.createReadStream(path.join(apos.rootDir, '/public/test-image.jpg')); formDataPortrait.append('file', streamPortrait); const attachmentPortrait = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', { body: formDataPortrait, jar }); image = await apos.http.put(`/api/v1/@apostrophecms/image/${image._id}`, { body: { title: 'Test Image Portrait', attachment: attachmentPortrait }, jar }); streamPortrait.close(); piece = await apos.http.get(`/api/v1/test-piece/${piece._id}`, { jar }); imageFields = piece.main.items[0].imageFields[image.aposDocId]; assert(imageFields, 'imageFields should be present after replacing the image attachment'); assert.equal(imageFields.width / imageFields.height, 3 / 2, 'aspect ratio should be 3:2'); await fsp.access( path.join( publicFolderPath, attachmentPortrait._urls.original.replace( '.jpg', `.${imageFields.left}.${imageFields.top}.${imageFields.width}.${imageFields.height}.jpg` ) ) ); }); async function insertUser(info) { const user = apos.user.newInstance(); assert(user); Object.assign(user, info); await apos.user.insert(apos.task.getReq(), user); } async function login(username, password) { if (!password) { password = username; } jar = apos.http.jar(); // establish session let page = await apos.http.get('/', { jar }); assert(page.match(/logged out/)); // Log in await apos.http.post('/api/v1/@apostrophecms/login/login', { body: { username, password, session: true }, jar }); // Confirm login page = await apos.http.get('/', { jar }); assert(page.match(/logged in/)); return jar; } async function getEditableImages(jar) { return (await apos.http.post('/api/v1/@apostrophecms/doc/editable?aposMode=draft', { body: { ids: inserted.map(doc => doc._id.replace(':published', ':draft')) }, jar })).editable; } }); describe('Image Lib', function() { const imageLib = require('../lib/image.js'); describe('computeMinSizes', function() { it('should return the min sizes if no aspect ratio is provided', function() { const result1 = imageLib.computeMinSizes([ 200, 100 ]); const result2 = imageLib.computeMinSizes([ 100, 200 ], 0); // ignore 0 assert.deepEqual(result1, { minWidth: 200, minHeight: 100 }); assert.deepEqual(result2, { minWidth: 100, minHeight: 200 }); }); it('should return the higher value for square aspect ratio', function() { const result1 = imageLib.computeMinSizes([ 200, 100 ], 1); const result2 = imageLib.computeMinSizes([ 100, 200 ], 1); assert.deepEqual(result1, { minWidth: 200, minHeight: 200 }); assert.deepEqual(result2, { minWidth: 200, minHeight: 200 }); }); it('should compute the min sizes for a wider aspect ratio', function() { const result1 = imageLib.computeMinSizes([ 1000, 300 ], [ 3, 1 ]); const result2 = imageLib.computeMinSizes([ 500, 2000 ], [ 3, 1 ]); const result3 = imageLib.computeMinSizes([ 100, 100 ], [ 3, 1 ]); const result4 = imageLib.computeMinSizes([ 30, 50 ], [ 3, 1 ]); const result5 = imageLib.computeMinSizes([ 600, 1800 ], [ 3, 1 ]); const result6 = imageLib.computeMinSizes([ 1800, 600 ], [ 3, 1 ]); assert.deepEqual(result1, { minWidth: 1000, minHeight: 1000 / 3 }); assert.deepEqual(result2, { minWidth: 6000, minHeight: 2000 }); assert.deepEqual(result3, { minWidth: 300, minHeight: 100 }); assert.deepEqual(result4, { minWidth: 150, minHeight: 50 }); assert.deepEqual(result5, { minWidth: 5400, minHeight: 1800 }); assert.deepEqual(result6, { minWidth: 1800, minHeight: 600 }); }); it('should compute the min sizes for a taller aspect ratio', function() { const result1 = imageLib.computeMinSizes([ 1000, 300 ], [ 1, 3 ]); const result2 = imageLib.computeMinSizes([ 500, 2000 ], [ 1, 3 ]); const result3 = imageLib.computeMinSizes([ 100, 100 ], [ 1, 3 ]); const result4 = imageLib.computeMinSizes([ 30, 50 ], [ 1, 3 ]); const result5 = imageLib.computeMinSizes([ 600, 1800 ], [ 1, 3 ]); const result6 = imageLib.computeMinSizes([ 1800, 600 ], [ 1, 3 ]); assert.deepEqual(result1, { minWidth: 1000, minHeight: 3000 }); assert.deepEqual(result2, { minWidth: 2000 / 3, minHeight: 2000 }); assert.deepEqual(result3, { minWidth: 100, minHeight: 300 }); assert.deepEqual(result4, { minWidth: 30, minHeight: 90 }); assert.deepEqual(result5, { minWidth: 600, minHeight: 1800 }); assert.deepEqual(result6, { minWidth: 1800, minHeight: 5400 }); }); }); });