Skip to content

Commit fa02a4d

Browse files
committed
Write cypress tests for ImageUploader
- have I missed any important features? too many tests?
1 parent 544b644 commit fa02a4d

File tree

7 files changed

+163
-32
lines changed

7 files changed

+163
-32
lines changed

backend/src/db/factories.js

+3
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ Factory.define('post')
113113
.attr('slug', ['slug', 'title'], (slug, title) => {
114114
return slug || slugify(title, { lower: true })
115115
})
116+
.attr('language', ['language'], language => {
117+
return language || 'en'
118+
})
116119
.after(async (buildObject, options) => {
117120
const [post, author, categories, tags] = await Promise.all([
118121
neode.create('Post', buildObject),

backend/src/schema/resolvers/fileUpload/index.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import { createWriteStream } from 'fs'
22
import path from 'path'
33
import slug from 'slug'
4+
import uuid from 'uuid/v4'
45

5-
const storeUpload = ({ createReadStream, fileLocation }) =>
6-
new Promise((resolve, reject) =>
6+
const localFileUpload = async ({ createReadStream, uniqueFilename }) => {
7+
await new Promise((resolve, reject) =>
78
createReadStream()
8-
.pipe(createWriteStream(`public${fileLocation}`))
9+
.pipe(createWriteStream(`public${uniqueFilename}`))
910
.on('finish', resolve)
1011
.on('error', reject),
1112
)
13+
return uniqueFilename
14+
}
1215

13-
export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) {
16+
export default async function fileUpload(params, { file, url }, uploadCallback = localFileUpload) {
1417
const upload = params[file]
1518
if (upload) {
1619
const { createReadStream, filename } = await upload
17-
const { name } = path.parse(filename)
18-
const fileLocation = `/uploads/${Date.now()}-${slug(name)}`
19-
await uploadCallback({ createReadStream, fileLocation })
20+
const { name, ext } = path.parse(filename)
21+
const uniqueFilename = `/uploads/${uuid()}-${slug(name)}${ext}`
22+
const location = await uploadCallback({ createReadStream, uniqueFilename })
2023
delete params[file]
21-
params[url] = fileLocation
24+
params[url] = location
2225
}
2326

2427
return params

backend/src/schema/resolvers/fileUpload/spec.js

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import fileUpload from '.'
22

3+
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}'
4+
35
describe('fileUpload', () => {
46
let params
57
let uploadCallback
@@ -13,7 +15,7 @@ describe('fileUpload', () => {
1315
createReadStream: jest.fn(),
1416
},
1517
}
16-
uploadCallback = jest.fn()
18+
uploadCallback = jest.fn(({ uniqueFilename }) => uniqueFilename)
1719
})
1820

1921
it('calls uploadCallback', async () => {
@@ -24,20 +26,13 @@ describe('fileUpload', () => {
2426
describe('file name', () => {
2527
it('saves the upload url in params[url]', async () => {
2628
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
27-
expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/)
28-
})
29-
30-
it('uses the name without file ending', async () => {
31-
params.uploadAttribute.filename = 'somePng.png'
32-
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
33-
expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/)
29+
expect(params.attribute).toMatch(new RegExp(`^/uploads/${uuid}-avatar.jpg`))
3430
})
3531

3632
it('creates a url safe name', async () => {
37-
params.uploadAttribute.filename =
38-
'/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar'
33+
params.uploadAttribute.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
3934
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
40-
expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/)
35+
expect(params.attribute).toMatch(new RegExp(`/uploads/${uuid}-foo-bar-avatar.jpg$`))
4136
})
4237

4338
describe('in case of duplicates', () => {
@@ -50,7 +45,6 @@ describe('fileUpload', () => {
5045
uploadCallback,
5146
)
5247

53-
await new Promise(resolve => setTimeout(resolve, 1000))
5448
const { attribute: second } = await fileUpload(
5549
{
5650
...params,

cypress/fixtures/humanconnection.png

130 KB
Loading

cypress/integration/common/post.js

+71
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { When, Then } from "cypress-cucumber-preprocessor/steps";
2+
import locales from '../../../webapp/locales'
3+
import orderBy from 'lodash/orderBy'
24

5+
const languages = orderBy(locales, 'name')
36
const narratorAvatar =
47
"https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg";
58

@@ -83,3 +86,71 @@ And("the post with title {string} has a ribbon for pinned posts", (title) => {
8386
Then("I see a toaster with {string}", (title) => {
8487
cy.get(".iziToast-message").should("contain", title);
8588
})
89+
90+
Then("I should be able to {string} a teaser image", condition => {
91+
let teaserImageUpload = "onourjourney.png";
92+
if (condition === 'change') teaserImageUpload = "humanconnection.png";
93+
cy.fixture(teaserImageUpload).as('postTeaserImage').then(function() {
94+
cy.get("#postdropzone").upload(
95+
{ fileContent: this.postTeaserImage, fileName: teaserImageUpload, mimeType: "image/png" },
96+
{ subjectType: "drag-n-drop", force: true }
97+
);
98+
})
99+
})
100+
101+
Then('confirm crop', () => {
102+
cy.get('.crop-confirm')
103+
.click()
104+
})
105+
106+
Then("I add all required fields", () => {
107+
cy.get('input[name="title"]')
108+
.type('new post')
109+
.get(".editor .ProseMirror")
110+
.type('new post content')
111+
.get(".base-button")
112+
.contains("Just for Fun")
113+
.click()
114+
.get('.ds-flex-item > .ds-form-item .ds-select ')
115+
.click()
116+
.get('.ds-select-option')
117+
.eq(languages.findIndex(l => l.code === 'en'))
118+
.click()
119+
})
120+
121+
Then("the post was saved successfully with the {string} teaser image", condition => {
122+
cy.get(".ds-card-content > .ds-heading")
123+
.should("contain", condition === 'new' ? 'new post' : 'to be updated')
124+
.get(".content")
125+
.should("contain", condition === 'new' ? 'new post content' : 'successfully updated')
126+
.get('.post-page img')
127+
.should("have.attr", "src")
128+
.and("contains", condition === 'new' ? "onourjourney" : "humanconnection")
129+
})
130+
131+
Then("the first image should be removed from the preview", () => {
132+
cy.fixture("humanconnection.png").as('postTeaserImage').then(function() {
133+
cy.get("#postdropzone")
134+
.children()
135+
.get('img.thumbnail-preview')
136+
.should('have.length', 1)
137+
.and('have.attr', 'src')
138+
.and('contain', this.postTeaserImage)
139+
})
140+
})
141+
142+
Then('the post was saved successfully without a teaser image', () => {
143+
cy.get(".ds-card-content > .ds-heading")
144+
.should("contain", 'new post')
145+
.get(".content")
146+
.should("contain", 'new post content')
147+
.get('.post-page')
148+
.should('exist')
149+
.get('.post-page img.ds-card-image')
150+
.should('not.exist')
151+
})
152+
153+
Then('I should be able to remove it', () => {
154+
cy.get('.crop-cancel')
155+
.click()
156+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Feature: Upload Teaser Image
2+
As a user
3+
I would like to be able to add a teaser image to my Post
4+
So that I can personalize my posts
5+
6+
7+
Background:
8+
Given I have a user account
9+
Given I am logged in
10+
Given we have the following posts in our database:
11+
| authorId | id | title | content |
12+
| id-of-peter-pan | p1 | Post to be updated | successfully updated |
13+
14+
Scenario: Create a Post with a Teaser Image
15+
When I click on the big plus icon in the bottom right corner to create post
16+
Then I should be able to "add" a teaser image
17+
And confirm crop
18+
And I add all required fields
19+
And I click on "Save"
20+
Then I get redirected to ".../new-post"
21+
And the post was saved successfully with the "new" teaser image
22+
23+
Scenario: Update a Post to add an image
24+
Given I am on the 'post/edit/p1' page
25+
And I should be able to "change" a teaser image
26+
And confirm crop
27+
And I click on "Save"
28+
Then I see a toaster with "Saved!"
29+
And I get redirected to ".../post-to-be-updated"
30+
Then the post was saved successfully with the "updated" teaser image
31+
32+
Scenario: Add image, then add a different image
33+
When I click on the big plus icon in the bottom right corner to create post
34+
Then I should be able to "add" a teaser image
35+
And confirm crop
36+
And I should be able to "change" a teaser image
37+
And confirm crop
38+
And the first image should be removed from the preview
39+
40+
Scenario: Add image, then delete it
41+
When I click on the big plus icon in the bottom right corner to create post
42+
Then I should be able to "add" a teaser image
43+
And I should be able to remove it
44+
And I add all required fields
45+
And I click on "Save"
46+
Then I get redirected to ".../new-post"
47+
And the post was saved successfully without a teaser image

webapp/components/TeaserImage/TeaserImage.vue

+25-12
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export default {
6868
oldImage: null,
6969
error: false,
7070
showCropper: false,
71+
imageAspectRatio: null,
7172
}
7273
},
7374
methods: {
@@ -113,29 +114,41 @@ export default {
113114
},
114115
cropImage() {
115116
this.showCropper = false
117+
if (this.file.type === 'image/png' || this.file.type === 'image/svg+xml') {
118+
this.uploadSvgOrPng()
119+
} else {
120+
this.uploadJpeg()
121+
}
122+
},
123+
uploadSvgOrPng() {
124+
this.imageAspectRatio = this.file.width / this.file.height || 1.0
125+
this.image = new Image()
126+
this.image.src = this.file.dataURL
127+
this.setupPreview()
128+
this.emitImageData(this.file)
129+
},
130+
uploadJpeg() {
116131
const canvas = this.cropper.getCroppedCanvas()
117132
canvas.toBlob(blob => {
118-
const imageAspectRatio = canvas.width / canvas.height
119-
this.setupPreview(canvas)
120-
this.removeCropper()
133+
this.imageAspectRatio = canvas.width / canvas.height
134+
this.image = new Image()
135+
this.image.src = canvas.toDataURL()
136+
this.setupPreview()
121137
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
122-
this.$emit('addTeaserImage', croppedImageFile)
123-
this.$emit('addImageAspectRatio', imageAspectRatio)
138+
this.emitImageData(croppedImageFile)
124139
}, 'image/jpeg')
125140
},
126-
setupPreview(canvas) {
127-
this.image = new Image()
128-
this.image.src = canvas.toDataURL()
141+
setupPreview() {
129142
this.image.classList.add('thumbnail-preview')
130143
this.thumbnailElement.appendChild(this.image)
131144
},
132145
cancelCrop() {
133-
this.showCropper = false
134146
if (this.oldImage) this.thumbnailElement.appendChild(this.oldImage)
135-
this.removeCropper()
147+
this.showCropper = false
136148
},
137-
removeCropper() {
138-
this.editor.removeChild(document.querySelectorAll('.cropper-container')[0])
149+
emitImageData(imageFile) {
150+
this.$emit('addTeaserImage', imageFile)
151+
this.$emit('addImageAspectRatio', this.imageAspectRatio)
139152
},
140153
},
141154
}

0 commit comments

Comments
 (0)