diff --git a/components/Editor.js b/components/Editor.js index f190629..ea802d9 100644 --- a/components/Editor.js +++ b/components/Editor.js @@ -38,6 +38,8 @@ const BackgroundSelect = dynamic(() => import('./BackgroundSelect'), { loading: () => null }) +const getConfig = omit(['code']) + class Editor extends React.Component { static contextType = ApiContext @@ -326,7 +328,7 @@ class Editor extends React.Component { exportSize } = this.state - const config = omit(this.state, ['code']) + const config = getConfig(this.state) const theme = this.getTheme() diff --git a/components/EditorContainer.js b/components/EditorContainer.js index 6df5085..e43402a 100644 --- a/components/EditorContainer.js +++ b/components/EditorContainer.js @@ -16,7 +16,7 @@ function EditorContainer(props) { }, []) React.useEffect(() => { - saveThemes(localStorage, themes.filter(({ custom }) => custom)) + saveThemes(themes.filter(({ custom }) => custom)) }, [themes]) return diff --git a/components/ImagePicker.js b/components/ImagePicker.js index 34cad37..ce5cb15 100644 --- a/components/ImagePicker.js +++ b/components/ImagePicker.js @@ -40,7 +40,8 @@ const INITIAL_STATE = { crop: null, imageAspectRatio: null, pixelCrop: null, - photographer: null + photographer: null, + dataURL: null } export default class ImagePicker extends React.Component { @@ -48,13 +49,14 @@ export default class ImagePicker extends React.Component { constructor(props) { super(props) this.state = INITIAL_STATE + this.selectMode = this.selectMode.bind(this) this.handleURLInput = this.handleURLInput.bind(this) + this.uploadImage = this.uploadImage.bind(this) this.selectImage = this.selectImage.bind(this) this.removeImage = this.removeImage.bind(this) this.onImageLoaded = this.onImageLoaded.bind(this) this.onCropChange = this.onCropChange.bind(this) this.onDragEnd = this.onDragEnd.bind(this) - this.selectMode = this.selectMode.bind(this) } static getDerivedStateFromProps(nextProps, state) { @@ -73,9 +75,13 @@ export default class ImagePicker extends React.Component { return null } + selectMode(mode) { + this.setState({ mode }) + } + async onDragEnd() { if (this.state.pixelCrop) { - const croppedImg = await getCroppedImg(this.props.imageDataURL, this.state.pixelCrop) + const croppedImg = await getCroppedImg(this.state.dataURL, this.state.pixelCrop) this.props.onChange({ backgroundImageSelection: croppedImg }) } } @@ -102,18 +108,23 @@ export default class ImagePicker extends React.Component { }) } + handleImageChange = (url, dataURL, photographer) => { + this.setState({ dataURL, photographer }, () => { + this.props.onChange({ + backgroundImage: url, + backgroundImageSelection: null, + photographer + }) + }) + } + handleURLInput(e) { e.preventDefault() const url = e.target[0].value return this.context .downloadThumbnailImage({ url }) - .then(({ dataURL }) => - this.props.onChange({ - backgroundImage: dataURL, - backgroundImageSelection: null, - photographer: null - }) - ) + .then(res => res.dataURL) + .then(dataURL => this.handleImageChange(url, dataURL)) .catch(err => { if (err.message.indexOf('Network Error') > -1) { this.setState({ @@ -124,22 +135,15 @@ export default class ImagePicker extends React.Component { }) } - selectMode(mode) { - this.setState({ mode }) + async uploadImage(e) { + const dataURL = await fileToDataURL(e.target.files[0]) + return this.handleImageChange(dataURL, dataURL) } - selectImage(e, { photographer } = {}) { - const file = e.target ? e.target.files[0] : e - - return fileToDataURL(file).then(dataURL => - this.setState({ photographer }, () => { - this.props.onChange({ - backgroundImage: dataURL, - backgroundImageSelection: null, - photographer - }) - }) - ) + async selectImage(url, { photographer } = {}) { + // TODO use React suspense for loading this asset + const { dataURL } = await this.context.downloadThumbnailImage({ url }) + return this.handleImageChange(url, dataURL, photographer) } removeImage() { @@ -172,7 +176,7 @@ export default class ImagePicker extends React.Component { ) : (
@@ -251,7 +255,7 @@ export default class ImagePicker extends React.Component { ) - if (this.props.imageDataURL) { + if (this.state.dataURL) { content = (
@@ -260,7 +264,7 @@ export default class ImagePicker extends React.Component {
{ const image = cacheRef.current[cacheIndex] - return api.unsplash.download(image.id).then(blob => props.onChange(blob, image)) + return api.unsplash.download(image.id).then(url => props.onChange(url, image)) }) const [updateCache, { loading: updating, error, data: imgs }] = useAsyncCallback( diff --git a/components/Settings.js b/components/Settings.js index 7b058e2..d661971 100644 --- a/components/Settings.js +++ b/components/Settings.js @@ -347,7 +347,7 @@ class Settings extends React.PureComponent { ) } - savePresets = () => savePresets(localStorage, this.state.presets.filter(p => p.custom)) + savePresets = () => savePresets(this.state.presets.filter(p => p.custom)) renderContent = () => { switch (this.state.selectedMenu) { diff --git a/cypress/integration/visual-testing.spec.js b/cypress/integration/visual-testing.spec.js new file mode 100644 index 0000000..2effb82 --- /dev/null +++ b/cypress/integration/visual-testing.spec.js @@ -0,0 +1,115 @@ +/* global cy, before, after */ +import { environment } from '../util' + +describe.skip('Visual regression testing', () => { + describe('Buttons', () => { + before(() => { + cy.eyesOpen({ + appName: 'Carbon', + testName: 'Button', + browser: environment + }) + cy.visit('/') + }) + + beforeEach(() => { + cy.reload() + }) + + after(() => { + cy.eyesClose() + }) + + it('test export button', () => { + cy.get('[data-cy=export-button]').click() + cy.eyesCheckWindow({ + tag: 'export button', + target: 'region', + selector: '.page' + }) + }) + + it('test display button', () => { + cy.get('[data-cy=display]').click() + cy.eyesCheckWindow({ + tag: 'display button', + target: 'region', + selector: '.page' + }) + }) + + it('test color button', () => { + cy.get('[data-cy=display]').click() + cy.wait(2000) + cy.get('[title="#50E3C2"]').click() + cy.wait(500) + cy.eyesCheckWindow({ + tag: 'color button', + target: 'region', + selector: '.page' + }) + }) + }) + + describe('Syntax', () => { + before(() => { + cy.eyesOpen({ + appName: 'Carbon', + testName: 'Syntax', + browser: environment + }) + }) + + after(() => { + cy.eyesClose() + }) + + const cases = [ + ['JSON', "/?code={name:'Andrew',age:30}&l=application%2Fjson"], + ['C#', '/?code=class Program { static void Main(){ do }}&l=text%2Fx-csharp'], + ['C++', '/?l=text%2Fx-c%2B%2Bsrc&code=for(size_t i=0 ;i { + it(`Syntax test for "${language}"`, () => { + cy.visit(example) + cy.eyesCheckWindow({ + tag: language, + target: 'region', + selector: '.page' + }) + }) + }) + }) + + describe('Themes', () => { + before(() => { + cy.eyesOpen({ + appName: 'Carbon', + testName: 'Syntax', + browser: environment + }) + }) + + after(() => { + cy.eyesClose() + }) + + const cases = [ + ['JSON', "/?code={name:'Andrew',age:30}&l=application%2Fjson"], + ['C#', '/?code=class Program { static void Main(){ do }}&l=text%2Fx-csharp'], + ['C++', '/?l=text%2Fx-c%2B%2Bsrc&code=for(size_t i=0 ;i { + it(`Syntax test for "${language}"`, () => { + cy.visit(example) + cy.eyesCheckWindow({ + tag: language, + target: 'region', + selector: '.page' + }) + }) + }) + }) +}) diff --git a/cypress/integration/visual-testing/button-test.spec.js b/cypress/integration/visual-testing/button-test.spec.js deleted file mode 100644 index 14a473e..0000000 --- a/cypress/integration/visual-testing/button-test.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -/* global cy, before, after */ -import { environment } from '../../util' - -describe('Visual Regression Testing', () => { - before(() => { - cy.eyesOpen({ - appName: 'Carbon', - testName: 'Button', - browser: environment - }) - cy.visit('/') - }) - - beforeEach(() => { - cy.reload() - }) - - after(() => { - cy.eyesClose() - }) - - it('test export button', () => { - cy.get('[data-cy=export-button]').click() - cy.eyesCheckWindow({ - tag: 'export button', - target: 'region', - selector: '.page' - }) - }) - - it('test display button', () => { - cy.get('[data-cy=display]').click() - cy.eyesCheckWindow({ - tag: 'display button', - target: 'region', - selector: '.page' - }) - }) - - it('test color button', () => { - cy.get('[data-cy=display]').click() - cy.wait(2000) - cy.get('[title="#50E3C2"]').click() - cy.wait(500) - cy.eyesCheckWindow({ - tag: 'color button', - target: 'region', - selector: '.page' - }) - }) -}) diff --git a/cypress/integration/visual-testing/syntax-test.spec.js b/cypress/integration/visual-testing/syntax-test.spec.js deleted file mode 100644 index 2e74f05..0000000 --- a/cypress/integration/visual-testing/syntax-test.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -/* global cy, before, after */ -import { environment } from '../../util' - -describe('Visual Regression Testing', () => { - before(() => { - cy.eyesOpen({ - appName: 'Carbon', - testName: 'Syntax', - browser: environment - }) - }) - - after(() => { - cy.eyesClose() - }) - - const cases = [ - ['JSON', "/?code={name:'Andrew',age:30}&l=application%2Fjson"], - ['C#', '/?code=class Program { static void Main(){ do }}&l=text%2Fx-csharp'], - ['C++', '/?l=text%2Fx-c%2B%2Bsrc&code=for(size_t i=0 ;i { - it(`Syntax test for "${language}"`, () => { - cy.visit(example) - cy.eyesCheckWindow({ - tag: language, - target: 'region', - selector: '.page' - }) - }) - }) -}) diff --git a/cypress/integration/visual-testing/theme-test.spec.js b/cypress/integration/visual-testing/theme-test.spec.js deleted file mode 100644 index e30239d..0000000 --- a/cypress/integration/visual-testing/theme-test.spec.js +++ /dev/null @@ -1,29 +0,0 @@ -/* global cy, before ,after */ -import { environment } from '../../util' -import { THEMES } from '../../../lib/constants' - -describe('Visual Regression Testing', () => { - before(() => { - cy.eyesOpen({ - appName: 'Carbon', - testName: 'Themes', - browser: environment - }) - cy.visit('/') - }) - - after(() => { - cy.eyesClose() - }) - - THEMES.forEach(t => { - it(`Test theme: "${t.name}"`, () => { - cy.get('[data-cy="themes-container"] [data-cy="theme-selector-button"]').click() - cy.contains(t.name).click({ force: true }) - cy.eyesCheckWindow({ - target: 'region', - selector: '.page' - }) - }) - }) -}) diff --git a/lib/api.js b/lib/api.js index 6a73339..5f92085 100644 --- a/lib/api.js +++ b/lib/api.js @@ -107,11 +107,7 @@ const downloadThumbnailImage = img => { const unsplash = { download(id) { - return client - .get(`/unsplash/download/${id}`) - .then(res => res.data.url) - .then(url => client.get(url, { responseType: 'blob' })) - .then(res => res.data) + return client.get(`/unsplash/download/${id}`).then(res => res.data.url) }, async random() { const imageUrls = await client.get('/unsplash/random') diff --git a/lib/util.js b/lib/util.js index f48dda2..1c6469d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -9,11 +9,20 @@ const THEMES_KEY = 'CARBON_THEMES' const createAssigner = key => { const assign = morph.assign(key) - return (window, v) => assign(window, JSON.stringify(v)) + return v => assign(localStorage, JSON.stringify(v)) } -export const saveSettings = createAssigner(SETTINGS_KEY) -export const savePresets = createAssigner(PRESETS_KEY) +const map = fn => obj => obj.map(fn) +export const omit = keys => object => omitBy(object, (_, k) => keys.indexOf(k) > -1) + +export const saveSettings = morph.compose( + createAssigner(SETTINGS_KEY), + omit(['code', 'backgroundImage', 'backgroundImageSelection', 'themes', 'highlights', 'fontUrl']) +) +export const savePresets = morph.compose( + createAssigner(PRESETS_KEY), + map(omit(['backgroundImageSelection'])) +) export const saveThemes = createAssigner(THEMES_KEY) const parse = v => { @@ -87,8 +96,6 @@ export const formatCode = async code => { }) } -export const omit = (object, keys) => omitBy(object, (_, k) => keys.indexOf(k) > -1) - export const stringifyRGBA = obj => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})` export const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1) diff --git a/pages/index.js b/pages/index.js index a9ca6bd..8ab1150 100644 --- a/pages/index.js +++ b/pages/index.js @@ -10,7 +10,7 @@ import EditorContainer from '../components/EditorContainer' import Page from '../components/Page' import { MetaLinks } from '../components/Meta' import { updateRouteState } from '../lib/routing' -import { saveSettings, clearSettings, omit } from '../lib/util' +import { saveSettings, clearSettings } from '../lib/util' class Index extends React.Component { componentDidMount() { @@ -25,17 +25,7 @@ class Index extends React.Component { onEditorUpdate = debounce( state => { updateRouteState(this.props.router, state) - saveSettings( - localStorage, - omit(state, [ - 'code', - 'backgroundImage', - 'backgroundImageSelection', - 'themes', - 'highlights', - 'fontUrl' - ]) - ) + saveSettings(state) }, 750, { trailing: true, leading: true }