mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #3056 from Human-Connection/3054-add-e2e-tests-image-uploader
test: Write cypress tests for ImageUploader
This commit is contained in:
commit
708edc06a3
@ -27,7 +27,7 @@ export const cleanDatabase = async (options = {}) => {
|
||||
Factory.define('category')
|
||||
.attr('id', uuid)
|
||||
.attr('icon', 'globe')
|
||||
.attr('name', 'global-peace-nonviolence')
|
||||
.attr('name', 'Global Peace & Nonviolence')
|
||||
.after((buildObject, options) => {
|
||||
return neode.create('Category', buildObject)
|
||||
})
|
||||
@ -113,6 +113,9 @@ Factory.define('post')
|
||||
.attr('slug', ['slug', 'title'], (slug, title) => {
|
||||
return slug || slugify(title, { lower: true })
|
||||
})
|
||||
.attr('language', ['language'], language => {
|
||||
return language || 'en'
|
||||
})
|
||||
.after(async (buildObject, options) => {
|
||||
const [post, author, categories, tags] = await Promise.all([
|
||||
neode.create('Post', buildObject),
|
||||
|
||||
@ -1,24 +1,27 @@
|
||||
import { createWriteStream } from 'fs'
|
||||
import path from 'path'
|
||||
import slug from 'slug'
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
const storeUpload = ({ createReadStream, fileLocation }) =>
|
||||
new Promise((resolve, reject) =>
|
||||
const localFileUpload = async ({ createReadStream, uniqueFilename }) => {
|
||||
await new Promise((resolve, reject) =>
|
||||
createReadStream()
|
||||
.pipe(createWriteStream(`public${fileLocation}`))
|
||||
.pipe(createWriteStream(`public${uniqueFilename}`))
|
||||
.on('finish', resolve)
|
||||
.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]
|
||||
if (upload) {
|
||||
const { createReadStream, filename } = await upload
|
||||
const { name } = path.parse(filename)
|
||||
const fileLocation = `/uploads/${Date.now()}-${slug(name)}`
|
||||
await uploadCallback({ createReadStream, fileLocation })
|
||||
const { name, ext } = path.parse(filename)
|
||||
const uniqueFilename = `/uploads/${uuid()}-${slug(name)}${ext}`
|
||||
const location = await uploadCallback({ createReadStream, uniqueFilename })
|
||||
delete params[file]
|
||||
params[url] = fileLocation
|
||||
params[url] = location
|
||||
}
|
||||
|
||||
return params
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
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', () => {
|
||||
let params
|
||||
let uploadCallback
|
||||
@ -13,7 +15,7 @@ describe('fileUpload', () => {
|
||||
createReadStream: jest.fn(),
|
||||
},
|
||||
}
|
||||
uploadCallback = jest.fn()
|
||||
uploadCallback = jest.fn(({ uniqueFilename }) => uniqueFilename)
|
||||
})
|
||||
|
||||
it('calls uploadCallback', async () => {
|
||||
@ -24,20 +26,13 @@ describe('fileUpload', () => {
|
||||
describe('file name', () => {
|
||||
it('saves the upload url in params[url]', async () => {
|
||||
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
|
||||
expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/)
|
||||
})
|
||||
|
||||
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/)
|
||||
expect(params.attribute).toMatch(new RegExp(`^/uploads/${uuid}-avatar.jpg`))
|
||||
})
|
||||
|
||||
it('creates a url safe name', async () => {
|
||||
params.uploadAttribute.filename =
|
||||
'/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar'
|
||||
params.uploadAttribute.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
|
||||
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', () => {
|
||||
@ -50,7 +45,6 @@ describe('fileUpload', () => {
|
||||
uploadCallback,
|
||||
)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
const { attribute: second } = await fileUpload(
|
||||
{
|
||||
...params,
|
||||
|
||||
@ -13,7 +13,7 @@ function createUser (slug) {
|
||||
return Factory.build('user', {
|
||||
name: slug,
|
||||
}, {
|
||||
password: '1234'
|
||||
password: '1234',
|
||||
email: 'example@test.org',
|
||||
})
|
||||
// await login({ email: 'example@test.org', password: '1234' })
|
||||
|
||||
BIN
cypress/fixtures/humanconnection.png
Normal file
BIN
cypress/fixtures/humanconnection.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@ -1,8 +1,11 @@
|
||||
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 =
|
||||
"https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg";
|
||||
|
||||
let expectedValue = { title: 'new post', content: 'new post content', src: 'onourjourney' }
|
||||
When("I type in a comment with {int} characters", size => {
|
||||
var c="";
|
||||
for (var i = 0; i < size; i++) {
|
||||
@ -83,3 +86,74 @@ And("the post with title {string} has a ribbon for pinned posts", (title) => {
|
||||
Then("I see a toaster with {string}", (title) => {
|
||||
cy.get(".iziToast-message").should("contain", title);
|
||||
})
|
||||
|
||||
Then("I should be able to {string} a teaser image", condition => {
|
||||
cy.reload()
|
||||
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(".categories-select .base-button")
|
||||
.first()
|
||||
.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 => {
|
||||
if (condition === 'updated')
|
||||
expectedValue = { title: 'to be updated', content: 'successfully updated', src: 'humanconnection' }
|
||||
cy.get(".ds-card-content > .ds-heading")
|
||||
.should("contain", expectedValue.title)
|
||||
.get(".content")
|
||||
.should("contain", expectedValue.content)
|
||||
.get('.post-page img')
|
||||
.should("have.attr", "src")
|
||||
.and("contains", expectedValue.src)
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
@ -48,6 +48,7 @@ Given("I am logged in", () => {
|
||||
});
|
||||
|
||||
Given("I log in as {string}", name => {
|
||||
cy.logout()
|
||||
cy.neode()
|
||||
.first("User", {
|
||||
name
|
||||
@ -237,25 +238,17 @@ Given("we have the following comments in our database:", table => {
|
||||
});
|
||||
|
||||
Given("we have the following posts in our database:", table => {
|
||||
cy.factory().build('category', {
|
||||
id: `cat-456`,
|
||||
name: "Just For Fun",
|
||||
slug: `just-for-fun`,
|
||||
icon: "smile"
|
||||
})
|
||||
|
||||
table.hashes().forEach((attributesOrOptions, i) => {
|
||||
cy.factory().build("post", {
|
||||
...attributesOrOptions,
|
||||
deleted: Boolean(attributesOrOptions.deleted),
|
||||
disabled: Boolean(attributesOrOptions.disabled),
|
||||
pinned: Boolean(attributesOrOptions.pinned),
|
||||
}, {
|
||||
...attributesOrOptions,
|
||||
categoryIds: ['cat-456']
|
||||
});
|
||||
})
|
||||
});
|
||||
table.hashes().forEach((attributesOrOptions, i) => {
|
||||
cy.factory().build("post", {
|
||||
...attributesOrOptions,
|
||||
deleted: Boolean(attributesOrOptions.deleted),
|
||||
disabled: Boolean(attributesOrOptions.disabled),
|
||||
pinned: Boolean(attributesOrOptions.pinned),
|
||||
}, {
|
||||
...attributesOrOptions,
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
Then("I see a success message:", message => {
|
||||
cy.contains(message);
|
||||
@ -269,6 +262,7 @@ When(
|
||||
"I click on the big plus icon in the bottom right corner to create post",
|
||||
() => {
|
||||
cy.get(".post-add-button").click();
|
||||
cy.location("pathname").should('eq', '/post/create')
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
47
cypress/integration/post/ImageUploader.feature
Normal file
47
cypress/integration/post/ImageUploader.feature
Normal 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
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="categories-select">
|
||||
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'xx-small' }">
|
||||
<div v-for="category in categories" :key="category.id">
|
||||
<ds-flex-item>
|
||||
|
||||
@ -68,6 +68,7 @@ export default {
|
||||
oldImage: null,
|
||||
error: false,
|
||||
showCropper: false,
|
||||
imageAspectRatio: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -113,29 +114,41 @@ export default {
|
||||
},
|
||||
cropImage() {
|
||||
this.showCropper = false
|
||||
if (this.file.type === 'image/jpeg') {
|
||||
this.uploadJpeg()
|
||||
} else {
|
||||
this.uploadOtherImageType()
|
||||
}
|
||||
},
|
||||
uploadOtherImageType() {
|
||||
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()
|
||||
canvas.toBlob(blob => {
|
||||
const imageAspectRatio = canvas.width / canvas.height
|
||||
this.setupPreview(canvas)
|
||||
this.removeCropper()
|
||||
this.imageAspectRatio = canvas.width / canvas.height
|
||||
this.image = new Image()
|
||||
this.image.src = canvas.toDataURL()
|
||||
this.setupPreview()
|
||||
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
|
||||
this.$emit('addTeaserImage', croppedImageFile)
|
||||
this.$emit('addImageAspectRatio', imageAspectRatio)
|
||||
this.emitImageData(croppedImageFile)
|
||||
}, 'image/jpeg')
|
||||
},
|
||||
setupPreview(canvas) {
|
||||
this.image = new Image()
|
||||
this.image.src = canvas.toDataURL()
|
||||
setupPreview() {
|
||||
this.image.classList.add('thumbnail-preview')
|
||||
this.thumbnailElement.appendChild(this.image)
|
||||
},
|
||||
cancelCrop() {
|
||||
this.showCropper = false
|
||||
if (this.oldImage) this.thumbnailElement.appendChild(this.oldImage)
|
||||
this.removeCropper()
|
||||
this.showCropper = false
|
||||
},
|
||||
removeCropper() {
|
||||
this.editor.removeChild(document.querySelectorAll('.cropper-container')[0])
|
||||
emitImageData(imageFile) {
|
||||
this.$emit('addTeaserImage', imageFile)
|
||||
this.$emit('addImageAspectRatio', this.imageAspectRatio)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user