Write cypress tests for ImageUploader

- have I missed any important features? too many tests?
This commit is contained in:
mattwr18 2020-02-12 17:53:38 +01:00
parent 544b64442a
commit fa02a4dd33
7 changed files with 163 additions and 32 deletions

View File

@ -113,6 +113,9 @@ Factory.define('post')
.attr('slug', ['slug', 'title'], (slug, title) => { .attr('slug', ['slug', 'title'], (slug, title) => {
return slug || slugify(title, { lower: true }) return slug || slugify(title, { lower: true })
}) })
.attr('language', ['language'], language => {
return language || 'en'
})
.after(async (buildObject, options) => { .after(async (buildObject, options) => {
const [post, author, categories, tags] = await Promise.all([ const [post, author, categories, tags] = await Promise.all([
neode.create('Post', buildObject), neode.create('Post', buildObject),

View File

@ -1,24 +1,27 @@
import { createWriteStream } from 'fs' import { createWriteStream } from 'fs'
import path from 'path' import path from 'path'
import slug from 'slug' import slug from 'slug'
import uuid from 'uuid/v4'
const storeUpload = ({ createReadStream, fileLocation }) => const localFileUpload = async ({ createReadStream, uniqueFilename }) => {
new Promise((resolve, reject) => await new Promise((resolve, reject) =>
createReadStream() createReadStream()
.pipe(createWriteStream(`public${fileLocation}`)) .pipe(createWriteStream(`public${uniqueFilename}`))
.on('finish', resolve) .on('finish', resolve)
.on('error', reject), .on('error', reject),
) )
return uniqueFilename
}
export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) { export default async function fileUpload(params, { file, url }, uploadCallback = localFileUpload) {
const upload = params[file] const upload = params[file]
if (upload) { if (upload) {
const { createReadStream, filename } = await upload const { createReadStream, filename } = await upload
const { name } = path.parse(filename) const { name, ext } = path.parse(filename)
const fileLocation = `/uploads/${Date.now()}-${slug(name)}` const uniqueFilename = `/uploads/${uuid()}-${slug(name)}${ext}`
await uploadCallback({ createReadStream, fileLocation }) const location = await uploadCallback({ createReadStream, uniqueFilename })
delete params[file] delete params[file]
params[url] = fileLocation params[url] = location
} }
return params return params

View File

@ -1,5 +1,7 @@
import fileUpload from '.' import fileUpload from '.'
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}'
describe('fileUpload', () => { describe('fileUpload', () => {
let params let params
let uploadCallback let uploadCallback
@ -13,7 +15,7 @@ describe('fileUpload', () => {
createReadStream: jest.fn(), createReadStream: jest.fn(),
}, },
} }
uploadCallback = jest.fn() uploadCallback = jest.fn(({ uniqueFilename }) => uniqueFilename)
}) })
it('calls uploadCallback', async () => { it('calls uploadCallback', async () => {
@ -24,20 +26,13 @@ describe('fileUpload', () => {
describe('file name', () => { describe('file name', () => {
it('saves the upload url in params[url]', async () => { it('saves the upload url in params[url]', async () => {
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/) expect(params.attribute).toMatch(new RegExp(`^/uploads/${uuid}-avatar.jpg`))
})
it('uses the name without file ending', async () => {
params.uploadAttribute.filename = 'somePng.png'
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/)
}) })
it('creates a url safe name', async () => { it('creates a url safe name', async () => {
params.uploadAttribute.filename = params.uploadAttribute.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
'/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar'
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback) await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/) expect(params.attribute).toMatch(new RegExp(`/uploads/${uuid}-foo-bar-avatar.jpg$`))
}) })
describe('in case of duplicates', () => { describe('in case of duplicates', () => {
@ -50,7 +45,6 @@ describe('fileUpload', () => {
uploadCallback, uploadCallback,
) )
await new Promise(resolve => setTimeout(resolve, 1000))
const { attribute: second } = await fileUpload( const { attribute: second } = await fileUpload(
{ {
...params, ...params,

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -1,5 +1,8 @@
import { When, Then } from "cypress-cucumber-preprocessor/steps"; import { When, Then } from "cypress-cucumber-preprocessor/steps";
import locales from '../../../webapp/locales'
import orderBy from 'lodash/orderBy'
const languages = orderBy(locales, 'name')
const narratorAvatar = const narratorAvatar =
"https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg"; "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg";
@ -83,3 +86,71 @@ And("the post with title {string} has a ribbon for pinned posts", (title) => {
Then("I see a toaster with {string}", (title) => { Then("I see a toaster with {string}", (title) => {
cy.get(".iziToast-message").should("contain", title); cy.get(".iziToast-message").should("contain", title);
}) })
Then("I should be able to {string} a teaser image", condition => {
let teaserImageUpload = "onourjourney.png";
if (condition === 'change') teaserImageUpload = "humanconnection.png";
cy.fixture(teaserImageUpload).as('postTeaserImage').then(function() {
cy.get("#postdropzone").upload(
{ fileContent: this.postTeaserImage, fileName: teaserImageUpload, mimeType: "image/png" },
{ subjectType: "drag-n-drop", force: true }
);
})
})
Then('confirm crop', () => {
cy.get('.crop-confirm')
.click()
})
Then("I add all required fields", () => {
cy.get('input[name="title"]')
.type('new post')
.get(".editor .ProseMirror")
.type('new post content')
.get(".base-button")
.contains("Just for Fun")
.click()
.get('.ds-flex-item > .ds-form-item .ds-select ')
.click()
.get('.ds-select-option')
.eq(languages.findIndex(l => l.code === 'en'))
.click()
})
Then("the post was saved successfully with the {string} teaser image", condition => {
cy.get(".ds-card-content > .ds-heading")
.should("contain", condition === 'new' ? 'new post' : 'to be updated')
.get(".content")
.should("contain", condition === 'new' ? 'new post content' : 'successfully updated')
.get('.post-page img')
.should("have.attr", "src")
.and("contains", condition === 'new' ? "onourjourney" : "humanconnection")
})
Then("the first image should be removed from the preview", () => {
cy.fixture("humanconnection.png").as('postTeaserImage').then(function() {
cy.get("#postdropzone")
.children()
.get('img.thumbnail-preview')
.should('have.length', 1)
.and('have.attr', 'src')
.and('contain', this.postTeaserImage)
})
})
Then('the post was saved successfully without a teaser image', () => {
cy.get(".ds-card-content > .ds-heading")
.should("contain", 'new post')
.get(".content")
.should("contain", 'new post content')
.get('.post-page')
.should('exist')
.get('.post-page img.ds-card-image')
.should('not.exist')
})
Then('I should be able to remove it', () => {
cy.get('.crop-cancel')
.click()
})

View File

@ -0,0 +1,47 @@
Feature: Upload Teaser Image
As a user
I would like to be able to add a teaser image to my Post
So that I can personalize my posts
Background:
Given I have a user account
Given I am logged in
Given we have the following posts in our database:
| authorId | id | title | content |
| id-of-peter-pan | p1 | Post to be updated | successfully updated |
Scenario: Create a Post with a Teaser Image
When I click on the big plus icon in the bottom right corner to create post
Then I should be able to "add" a teaser image
And confirm crop
And I add all required fields
And I click on "Save"
Then I get redirected to ".../new-post"
And the post was saved successfully with the "new" teaser image
Scenario: Update a Post to add an image
Given I am on the 'post/edit/p1' page
And I should be able to "change" a teaser image
And confirm crop
And I click on "Save"
Then I see a toaster with "Saved!"
And I get redirected to ".../post-to-be-updated"
Then the post was saved successfully with the "updated" teaser image
Scenario: Add image, then add a different image
When I click on the big plus icon in the bottom right corner to create post
Then I should be able to "add" a teaser image
And confirm crop
And I should be able to "change" a teaser image
And confirm crop
And the first image should be removed from the preview
Scenario: Add image, then delete it
When I click on the big plus icon in the bottom right corner to create post
Then I should be able to "add" a teaser image
And I should be able to remove it
And I add all required fields
And I click on "Save"
Then I get redirected to ".../new-post"
And the post was saved successfully without a teaser image

View File

@ -68,6 +68,7 @@ export default {
oldImage: null, oldImage: null,
error: false, error: false,
showCropper: false, showCropper: false,
imageAspectRatio: null,
} }
}, },
methods: { methods: {
@ -113,29 +114,41 @@ export default {
}, },
cropImage() { cropImage() {
this.showCropper = false this.showCropper = false
if (this.file.type === 'image/png' || this.file.type === 'image/svg+xml') {
this.uploadSvgOrPng()
} else {
this.uploadJpeg()
}
},
uploadSvgOrPng() {
this.imageAspectRatio = this.file.width / this.file.height || 1.0
this.image = new Image()
this.image.src = this.file.dataURL
this.setupPreview()
this.emitImageData(this.file)
},
uploadJpeg() {
const canvas = this.cropper.getCroppedCanvas() const canvas = this.cropper.getCroppedCanvas()
canvas.toBlob(blob => { canvas.toBlob(blob => {
const imageAspectRatio = canvas.width / canvas.height this.imageAspectRatio = canvas.width / canvas.height
this.setupPreview(canvas) this.image = new Image()
this.removeCropper() this.image.src = canvas.toDataURL()
this.setupPreview()
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type }) const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
this.$emit('addTeaserImage', croppedImageFile) this.emitImageData(croppedImageFile)
this.$emit('addImageAspectRatio', imageAspectRatio)
}, 'image/jpeg') }, 'image/jpeg')
}, },
setupPreview(canvas) { setupPreview() {
this.image = new Image()
this.image.src = canvas.toDataURL()
this.image.classList.add('thumbnail-preview') this.image.classList.add('thumbnail-preview')
this.thumbnailElement.appendChild(this.image) this.thumbnailElement.appendChild(this.image)
}, },
cancelCrop() { cancelCrop() {
this.showCropper = false
if (this.oldImage) this.thumbnailElement.appendChild(this.oldImage) if (this.oldImage) this.thumbnailElement.appendChild(this.oldImage)
this.removeCropper() this.showCropper = false
}, },
removeCropper() { emitImageData(imageFile) {
this.editor.removeChild(document.querySelectorAll('.cropper-container')[0]) this.$emit('addTeaserImage', imageFile)
this.$emit('addImageAspectRatio', this.imageAspectRatio)
}, },
}, },
} }