diff --git a/gulpfile.js b/gulpfile.js index 859973bc4f8..4bd95fca8c1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,4 +1,4 @@ -/* eslint-disable import/no-commonjs, import/no-unassigned-import */ +/* eslint-disable import/no-commonjs, import/no-unassigned-import, max-nested-callbacks */ // Note: Node 8 compat, please! require('hard-rejection/register') @@ -17,6 +17,9 @@ const through = require('through2') const chalk = require('chalk') const globby = require('globby') const mergeStream = require('merge-stream') +const backstop = require('backstopjs') +const waitPort = require('wait-port') +const kill = require('kill-port') const isWindows = /^win/.test(process.platform) @@ -190,7 +193,8 @@ const STUDIOS = [ {name: 'movies-studio', port: '3334'}, {name: 'example-studio', port: '3335'}, {name: 'blog-studio', port: '3336'}, - {name: 'ecommerce-studio', port: '3337'} + {name: 'ecommerce-studio', port: '3337'}, + {name: 'backstop-test-studio', port: '5000'} ] STUDIOS.forEach(studio => { @@ -238,3 +242,84 @@ gulp.task('storybook', ['watch-js', 'watch-ts', 'watch-assets'], () => { proc.stdout.pipe(process.stdout) proc.stderr.pipe(process.stderr) }) + +gulp.task('backstop', cb => { + const {exec} = require('child_process') + + exec('docker -v', (err, stdout, stderr) => { + if (err) { + throw new gutil.PluginError({ + plugin: 'backstop', + message: gutil.colors.red('Please install Docker on your computer. https://www.docker.com/') + }) + } + }) + + gulp.start('backstop-test-studio') + + const params = { + host: 'localhost', + port: 5000, + timeout: 100 * 60 * 10 + } + + waitPort(params) + .then(open => { + if (open) { + backstop('test', { + docker: true, + config: './test/backstop/backstop.js' + }) + .then(() => { + kill(params.port).then(() => { + gutil.log(gutil.colors.green('Backstop test success')) + // eslint-disable-next-line + process.exit(0) + }) + }) + .catch(() => { + kill(params.port) + throw new gutil.PluginError({ + plugin: 'backstop', + message: 'Tests failed' + }) + }) + } else { + kill(params.port) + throw new gutil.PluginError({ + plugin: 'backstop', + message: 'The backstop-studio did not start' + }) + } + }) + .catch(err => { + kill(params.port) + throw new gutil.PluginError({ + plugin: 'backstop', + message: `An unknown error occured while waiting for the port: ${err}` + }) + }) +}) + +gulp.task('backstop:approve', cb => { + backstop('approve', { + docker: true, + config: './test/backstop/backstop.js' + }) +}) + +gulp.task('backstop:reference', cb => { + backstop('reference', { + docker: true, + config: './test/backstop/backstop.js' + }) + .then(() => { + gutil.log(gutil.colors.green('References created')) + }) + .catch(() => { + throw new gutil.PluginError({ + plugin: 'backstop', + message: 'Making references failed' + }) + }) +}) diff --git a/lerna.json b/lerna.json index e8f4f0d6f3f..2819853dac7 100644 --- a/lerna.json +++ b/lerna.json @@ -8,6 +8,7 @@ "packages/create-sanity", "packages/ecommerce-studio", "packages/eslint-config-sanity", + "packages/backstop-test-studio", "packages/example-studio", "packages/groq", "packages/movies-studio", diff --git a/package.json b/package.json index d6286a9a895..02ab0780235 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "start": "npm run test-studio", "ecommerce-studio": "gulp ecommerce-studio", "test-studio": "gulp test-studio", + "backstop-test-studio": "gulp backstop-test-studio", "example-studio": "gulp example-studio", "movies-studio": "gulp movies-studio", "blog-studio": "gulp blog-studio", @@ -27,6 +28,9 @@ "stylelint:fix": "stylelint \"**/*.css\" --fix", "test": "echo 'Run `npm run test-all` to run `npm test` for every package'", "test-all": "lerna run test --stream", + "test:backstop": "gulp backstop", + "test:backstop:approve": "gulp backstop:approve", + "test:backstop:reference": "gulp backstop:reference", "updated": "lerna updated", "watch": "gulp watch", "install-husky": "node node_modules/husky/bin/install", @@ -53,6 +57,7 @@ "babel-plugin-css-modules-transform": "^1.6.1", "babel-plugin-istanbul": "^5.0.1", "babel-plugin-lodash": "^3.3.4", + "backstopjs": "^3.8.5", "boxen": "^1.3.0", "chalk": "^2.3.0", "deepmerge": "^2.0.1", @@ -80,6 +85,7 @@ "gulp-watch": "^5.0.1", "hard-rejection": "^1.0.0", "husky": "^0.14.3", + "kill-port": "^1.3.2", "lerna": "^3.4.0", "merge-stream": "^1.0.1", "minimist": "^1.2.0", @@ -93,6 +99,7 @@ "stylelint-config-standard": "^18.0.0", "through2": "^2.0.3", "typescript": "^3.2.2", + "wait-port": "^0.2.2", "yarn": "^1.3.2" } } diff --git a/packages/backstop-test-studio/.gitignore b/packages/backstop-test-studio/.gitignore new file mode 100644 index 00000000000..de05cffc3b1 --- /dev/null +++ b/packages/backstop-test-studio/.gitignore @@ -0,0 +1,12 @@ +# Logs +logs +*.log + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules + +# Built assets +dist diff --git a/packages/backstop-test-studio/config/.checksums b/packages/backstop-test-studio/config/.checksums new file mode 100644 index 00000000000..759750f25b2 --- /dev/null +++ b/packages/backstop-test-studio/config/.checksums @@ -0,0 +1,8 @@ +{ + "#": "Used by Sanity to keep track of configuration file checksums, do not delete or modify!", + "@sanity/default-layout": "bb034f391ba508a6ca8cd971967cbedeb131c4d19b17b28a0895f32db5d568ea", + "@sanity/data-aspects": "ba5c2649cc1b1c39ae92b7daf2661f95fa79d7325073ffd410245d2717b240e9", + "@sanity/storybook": "526dea3b461fda217e7150d12395d0ec639cba0155c05a084b85bcf2c44995a3", + "@sanity/google-maps-input": "57ae3a403ce6a070b31ec6fa1f3c8339cafa66661eaddba1d4d5ee3cc2197ec2", + "@sanity/default-login": "6fb6d3800aa71346e1b84d95bbcaa287879456f2922372bb0294e30b968cd37f" +} diff --git a/packages/backstop-test-studio/config/@sanity/data-aspects.json b/packages/backstop-test-studio/config/@sanity/data-aspects.json new file mode 100644 index 00000000000..d64838e727e --- /dev/null +++ b/packages/backstop-test-studio/config/@sanity/data-aspects.json @@ -0,0 +1,3 @@ +{ + "listOptions": {} +} diff --git a/packages/backstop-test-studio/config/@sanity/default-layout.json b/packages/backstop-test-studio/config/@sanity/default-layout.json new file mode 100644 index 00000000000..4d8b87f4d52 --- /dev/null +++ b/packages/backstop-test-studio/config/@sanity/default-layout.json @@ -0,0 +1,6 @@ +{ + "toolSwitcher": { + "order": [], + "hidden": [] + } +} diff --git a/packages/backstop-test-studio/config/@sanity/default-login.json b/packages/backstop-test-studio/config/@sanity/default-login.json new file mode 100644 index 00000000000..2680ad8bae8 --- /dev/null +++ b/packages/backstop-test-studio/config/@sanity/default-login.json @@ -0,0 +1,7 @@ +{ + "providers": { + "mode": "append", + "redirectOnSingle": false, + "entries": [] + } +} diff --git a/packages/backstop-test-studio/config/@sanity/google-maps-input.json b/packages/backstop-test-studio/config/@sanity/google-maps-input.json new file mode 100644 index 00000000000..53169a4321e --- /dev/null +++ b/packages/backstop-test-studio/config/@sanity/google-maps-input.json @@ -0,0 +1,8 @@ +{ + "apiKey": "AIzaSyDwxpdp6WJwj9VjoVCjEudljhkR1inzhSE", + "defaultZoom": 11, + "defaultLocation": { + "lat": 40.7058254, + "lng": -74.1180863 + } +} diff --git a/packages/backstop-test-studio/config/@sanity/storybook.json b/packages/backstop-test-studio/config/@sanity/storybook.json new file mode 100644 index 00000000000..f53ca638e30 --- /dev/null +++ b/packages/backstop-test-studio/config/@sanity/storybook.json @@ -0,0 +1,3 @@ +{ + "port": 9001 +} diff --git a/packages/backstop-test-studio/data/norway.js b/packages/backstop-test-studio/data/norway.js new file mode 100644 index 00000000000..0623c81d2e5 --- /dev/null +++ b/packages/backstop-test-studio/data/norway.js @@ -0,0 +1,116 @@ +export default [ + { + type: 'Feature', + id: 'NOR', + properties: {name: 'Norway'}, + geometry: { + type: 'MultiPolygon', + coordinates: [ + [ + [ + [28.165547, 71.185474], + [31.293418, 70.453788], + [30.005435, 70.186259], + [31.101079, 69.55808], + [29.399581, 69.156916], + [28.59193, 69.064777], + [29.015573, 69.766491], + [27.732292, 70.164193], + [26.179622, 69.825299], + [25.689213, 69.092114], + [24.735679, 68.649557], + [23.66205, 68.891247], + [22.356238, 68.841741], + [21.244936, 69.370443], + [20.645593, 69.106247], + [20.025269, 69.065139], + [19.87856, 68.407194], + [17.993868, 68.567391], + [17.729182, 68.010552], + [16.768879, 68.013937], + [16.108712, 67.302456], + [15.108411, 66.193867], + [13.55569, 64.787028], + [13.919905, 64.445421], + [13.571916, 64.049114], + [12.579935, 64.066219], + [11.930569, 63.128318], + [11.992064, 61.800362], + [12.631147, 61.293572], + [12.300366, 60.117933], + [11.468272, 59.432393], + [11.027369, 58.856149], + [10.356557, 59.469807], + [8.382, 58.313288], + [7.048748, 58.078884], + [5.665835, 58.588155], + [5.308234, 59.663232], + [4.992078, 61.970998], + [5.9129, 62.614473], + [8.553411, 63.454008], + [10.527709, 64.486038], + [12.358347, 65.879726], + [14.761146, 67.810642], + [16.435927, 68.563205], + [19.184028, 69.817444], + [21.378416, 70.255169], + [23.023742, 70.202072], + [24.546543, 71.030497], + [26.37005, 70.986262], + [28.165547, 71.185474] + ] + ], + [ + [ + [24.72412, 77.85385], + [22.49032, 77.44493], + [20.72601, 77.67704], + [21.41611, 77.93504], + [20.8119, 78.25463], + [22.88426, 78.45494], + [23.28134, 78.07954], + [24.72412, 77.85385] + ] + ], + [ + [ + [18.25183, 79.70175], + [21.54383, 78.95611], + [19.02737, 78.5626], + [18.47172, 77.82669], + [17.59441, 77.63796], + [17.1182, 76.80941], + [15.91315, 76.77045], + [13.76259, 77.38035], + [14.66956, 77.73565], + [13.1706, 78.02493], + [11.22231, 78.8693], + [10.44453, 79.65239], + [13.17077, 80.01046], + [13.71852, 79.66039], + [15.14282, 79.67431], + [15.52255, 80.01608], + [16.99085, 80.05086], + [18.25183, 79.70175] + ] + ], + [ + [ + [25.447625, 80.40734], + [27.407506, 80.056406], + [25.924651, 79.517834], + [23.024466, 79.400012], + [20.075188, 79.566823], + [19.897266, 79.842362], + [18.462264, 79.85988], + [17.368015, 80.318896], + [20.455992, 80.598156], + [21.907945, 80.357679], + [22.919253, 80.657144], + [25.447625, 80.40734] + ] + ] + ] + } + } +] diff --git a/packages/backstop-test-studio/package.json b/packages/backstop-test-studio/package.json new file mode 100644 index 00000000000..c8cc00895cf --- /dev/null +++ b/packages/backstop-test-studio/package.json @@ -0,0 +1,60 @@ +{ + "name": "backstop-test-studio", + "private": true, + "version": "0.139.0", + "description": "A bloated, ugly Sanity studio with a contrived schema for edge case testing and development", + "author": "Sanity.io ", + "license": "MIT", + "scripts": { + "start": "sanity start", + "clean": "rimraf dist" + }, + "keywords": [ + "sanity", + "cms", + "headless", + "realtime", + "content", + "test-studio" + ], + "dependencies": { + "@sanity/base": "0.139.6", + "@sanity/cli": "0.139.6", + "@sanity/code-input": "0.139.0", + "@sanity/color-input": "0.139.0", + "@sanity/components": "0.139.6", + "@sanity/core": "0.139.6", + "@sanity/default-layout": "0.139.6", + "@sanity/default-login": "0.139.0", + "@sanity/desk-tool": "0.139.6", + "@sanity/form-builder": "0.139.6", + "@sanity/google-maps-input": "0.139.6", + "@sanity/image-url": "0.139.0", + "@sanity/production-preview": "0.139.0", + "@sanity/rich-date-input": "0.139.0", + "@sanity/storybook": "0.139.3", + "@sanity/vision": "0.139.6", + "@turf/helpers": "^6.0.1", + "@turf/points-within-polygon": "^5.1.5", + "bio-pv": "^1.8.1", + "date-fns": "^1.29.0", + "fetch": "^1.1.0", + "get-video-id": "^2.1.4", + "humanize-list": "^1.0.1", + "prop-types": "^15.6.0", + "react": "^16.2.0", + "react-dom": "^16.2.0", + "react-icons": "^2.2.7" + }, + "devDependencies": { + "rimraf": "^2.6.2" + }, + "bugs": { + "url": "https://github.com/sanity-io/sanity/issues" + }, + "homepage": "https://www.sanity.io/", + "repository": { + "type": "git", + "url": "git+https://github.com/sanity-io/sanity.git" + } +} diff --git a/packages/backstop-test-studio/sanity.json b/packages/backstop-test-studio/sanity.json new file mode 100644 index 00000000000..5291152c283 --- /dev/null +++ b/packages/backstop-test-studio/sanity.json @@ -0,0 +1,41 @@ +{ + "root": true, + "project": { + "name": "Backstop Test Studio" + }, + "api": { + "projectId": "xtlrc4xo", + "dataset": "production" + }, + "plugins": [ + "@sanity/base", + "@sanity/components", + "@sanity/default-layout", + "@sanity/default-login", + "@sanity/code-input", + "@sanity/color-input", + "@sanity/desk-tool" + ], + "parts": [ + { + "name": "part:@sanity/base/schema", + "path": "./schemas/schema.js" + }, + { + "implements": "part:@sanity/base/schema-type", + "path": "src/components/PertEstimateSchema.js" + }, + { + "implements": "part:@sanity/production-preview/resolve-production-url", + "path": "src/resolveProductionUrl" + }, + { + "implements": "part:@sanity/form-builder/input/array/functions", + "path": "src/components/SearchableArrayFunctions.js" + }, + { + "name": "part:@sanity/desk-tool/structure", + "path": "src/deskStructure.js" + } + ] +} \ No newline at end of file diff --git a/packages/backstop-test-studio/schemas/arrays.js b/packages/backstop-test-studio/schemas/arrays.js new file mode 100644 index 00000000000..3f0bef6316f --- /dev/null +++ b/packages/backstop-test-studio/schemas/arrays.js @@ -0,0 +1,345 @@ +import React from 'react' +import icon from 'react-icons/lib/md/format-list-numbered' + +export const topLevelArrayType = { + name: 'topLevelArrayType', + type: 'array', + of: [ + { + type: 'object', + title: 'Content', + fields: [{name: 'textContent', type: 'text'}, {name: 'imageContent', type: 'image'}], + preview: {select: {title: 'textContent'}} + } + ] +} +export const topLevelPrimitiveArrayType = { + name: 'topLevelPrimitiveArrayType', + type: 'array', + of: [ + { + type: 'string', + title: 'A string' + }, + { + type: 'number', + title: 'A number' + } + ] +} + +export default { + name: 'arraysTest', + type: 'document', + title: 'Arrays test', + icon, + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'arrayOfMultipleTypes', + title: 'Array of multiple types', + type: 'array', + of: [ + { + type: 'image' + }, + { + type: 'book' + }, + { + type: 'object', + name: 'color', + title: 'Color with a long title', + fields: [ + { + name: 'title', + type: 'string' + }, + { + name: 'name', + type: 'string' + } + ] + } + ] + }, + { + name: 'arrayOfPredefinedOptions', + title: 'Array of predefined options', + description: [ + 'It should be possible to check/uncheck the different options.', + 'There should be a warning about invalid type (number)', + 'When inspecting a document with checked values, the array should contain values with: ', + '{_type: "color", ...}' + ].join('\n'), + type: 'array', + of: [ + { + type: 'object', + name: 'color', + fields: [ + { + name: 'title', + type: 'string' + }, + { + name: 'name', + type: 'string' + } + ], + preview: { + select: { + title: 'title', + name: 'name' + }, + prepare({title, name}) { + return { + title: title, + media: () => ( +
+ ) + } + } + } + } + ], + options: { + direction: 'vertical', + list: [ + {_type: 'color', title: 'Red', name: 'red', _key: 'red'}, + {_type: 'color', title: 'Green', name: 'green', _key: 'green'}, + 1, // invalid, not defined in list + {_type: 'color', title: 'Blue', name: 'blue', _key: 'blue'}, + {_type: 'color', title: 'Black', name: 'black', _key: 'black'} + ] + } + }, + { + name: 'tags', + title: 'Tags', + description: + 'Enter a tag and press enter. Should result in an array of strings and should be possible to remove items', + type: 'array', + options: {layout: 'tags'}, + of: [{type: 'string'}] + }, + { + name: 'arrayWithAnonymousObject', + title: 'Array with anonymous objects', + description: 'This array contains objects of type as defined inline', + type: 'array', + of: [ + { + type: 'object', + title: 'Something', + fields: [ + {name: 'first', type: 'string', title: 'First string'}, + {name: 'second', type: 'string', title: 'Second string'} + ] + } + ] + }, + { + name: 'arrayOfPrimitives', + title: 'Array with primitive types', + description: 'This array contains only strings, values and booleans', + type: 'array', + of: [ + { + type: 'string', + title: 'String' + }, + { + type: 'number', + title: 'Number' + }, + { + type: 'boolean', + title: 'Boolean' + } + ] + }, + { + name: 'arrayOfEmails', + title: 'Array of email addresses', + description: 'This array contains only email addresses', + type: 'array', + of: [ + { + type: 'email', + title: '' + } + ] + }, + { + name: 'arrayOfStringsWithLegacyList', + title: 'Array of strings with legacy format on lists', + description: + 'Previously the `list` option took an array of {title, value} items. It should still be possible to check these values.', + type: 'array', + of: [{type: 'string'}], + options: { + list: [ + {value: 'residential', title: 'Residential'}, + {value: 'education', title: 'Education'}, + {value: 'commercial', title: 'Commercial'}, + {value: 'cultural', title: 'Cultural'}, + {value: 'display', title: 'Display'}, + {value: 'installation', title: 'Installation'}, + {value: 'objects', title: 'Objects'}, + {value: 'performance', title: 'Performance'}, + {value: 'public space', title: 'Public Space'}, + {value: 'publications', title: 'Publications'} + ] + } + }, + { + name: 'fieldOfTopLevelArrayType', + title: 'Field of top level array type', + type: 'topLevelArrayType' + }, + { + name: 'fieldOfTopLevelPrimitiveArrayType', + title: 'Field of top level primitive array type', + type: 'topLevelPrimitiveArrayType' + }, + { + name: 'imageArrayInGrid', + title: 'Image array', + description: 'An array of images. options: {layout: "grid"}', + type: 'array', + options: { + layout: 'grid' + }, + of: [ + { + name: 'myImage', + title: 'My Image', + type: 'myImage' + } + ] + }, + { + name: 'imageArray', + title: 'Image array (with defaults)', + type: 'array', + of: [ + { + title: 'Image', + type: 'image', + preview: { + select: { + imageUrl: 'asset.url', + title: 'caption' + } + }, + fields: [ + { + name: 'caption', + type: 'string', + title: 'Caption', + options: { + isHighlighted: true + } + } + ] + } + ] + }, + { + name: 'polymorphicGridArray', + title: 'Polymorphic grid array', + description: 'An array of multiple types. options: {layout: "grid"}', + type: 'array', + options: { + layout: 'grid' + }, + of: [ + { + title: 'A book', + type: 'book' + }, + { + title: 'An author', + type: 'author' + }, + { + title: 'An image', + type: 'image' + } + ] + }, + { + name: 'imageArrayNotSortable', + title: 'Image array in grid, *not* sortable', + description: 'Images here should be append-only', + type: 'array', + options: { + sortable: false, + layout: 'grid' + }, + of: [ + { + title: 'Image', + type: 'image', + preview: { + select: { + imageUrl: 'asset.url', + title: 'caption' + } + }, + fields: [ + { + name: 'caption', + type: 'string', + title: 'Caption', + options: { + isHighlighted: true + } + } + ] + } + ] + }, + { + name: 'arrayOfNamedReferences', + type: 'array', + title: 'Array of named references', + description: 'The values here should get _type == authorReference or _type == bookReference', + of: [ + { + type: 'reference', + name: 'authorReference', + to: [{type: 'author', title: 'Reference to author'}] + }, + { + type: 'reference', + name: 'bookReference', + to: [{type: 'book', title: 'Reference to book'}] + } + ] + }, + { + name: 'arrayOfSoManyDifferentTypes', + type: 'array', + title: 'Array of SO MANY different types', + describe: 'Uses custom "Add" functionality', + of: ['author', 'book', 'code', 'color', 'geopoint', 'image', 'slug', 'species'].map(type => ({ + type + })) + } + ] +} diff --git a/packages/backstop-test-studio/schemas/author.js b/packages/backstop-test-studio/schemas/author.js new file mode 100644 index 00000000000..0dfedd82298 --- /dev/null +++ b/packages/backstop-test-studio/schemas/author.js @@ -0,0 +1,89 @@ +import icon from 'react-icons/lib/md/person' + +export default { + name: 'author', + type: 'document', + title: 'Author', + icon, + description: 'This represents an author', + preview: { + select: { + title: 'name', + awards: 'awards', + relatedAuthors: 'relatedAuthors', + lastUpdated: '_updatedAt', + media: 'image' + }, + prepare({title, media, awards}) { + return { + title: title, + media: media, + subtitle: awards && awards.join(', ') + } + } + }, + fields: [ + { + name: 'name', + title: 'Name', + type: 'string', + validation: Rule => Rule.required() + }, + { + name: 'bestFriend', + title: 'Best friend', + type: 'reference', + to: [{type: 'author'}] + }, + { + name: 'image', + title: 'Image', + type: 'image', + options: {hotspot: true} + }, + { + name: 'awards', + title: 'Awards', + type: 'array', + of: [ + { + type: 'string' + } + ] + }, + { + name: 'favoriteBooks', + title: 'Favorite books', + type: 'array', + of: [ + { + type: 'reference', + to: {type: 'book'} + } + ] + }, + { + name: 'minimalBlock', + title: 'Reset all options', + type: 'array', + of: [ + { + type: 'block', + styles: [], + lists: [], + marks: { + decorators: [], + annotations: [] + } + } + ] + }, + { + name: 'specie', + title: 'Specie', + description: 'There are a lot of species. Use this to test reference search performence', + type: 'reference', + to: {type: 'species'} + } + ] +} diff --git a/packages/backstop-test-studio/schemas/blocks.js b/packages/backstop-test-studio/schemas/blocks.js new file mode 100644 index 00000000000..0be5b8cf211 --- /dev/null +++ b/packages/backstop-test-studio/schemas/blocks.js @@ -0,0 +1,324 @@ +import icon from 'react-icons/lib/md/rate-review' + +export default { + name: 'blocksTest', + title: 'Blocks test', + type: 'document', + icon, + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'defaults', + title: 'Content', + description: 'Profound description of what belongs here', + type: 'array', + of: [ + {type: 'image', title: 'Image'}, + { + type: 'reference', + name: 'authorReference', + to: {type: 'author'}, + title: 'Reference to author' + }, + { + type: 'reference', + name: 'bookReference', + to: {type: 'book'}, + title: 'Reference to book' + }, + {type: 'author', title: 'Embedded author'}, + {type: 'code', title: 'Code'}, + { + type: 'color', + name: 'colorBlock', + title: 'Color (block)' + }, + { + type: 'object', + title: 'Test object', + name: 'testObject', + fields: [{name: 'field1', type: 'string'}] + }, + { + type: 'object', + title: 'Other test object', + name: 'otherTestObject', + fields: [ + {name: 'field1', type: 'string'}, + { + name: 'field3', + type: 'array', + of: [ + { + type: 'object', + fields: [{name: 'aString', type: 'string'}, {name: 'aNumber', type: 'number'}] + } + ] + } + ] + }, + { + type: 'block', + of: [ + { + type: 'color', + title: 'Color' + } + ] + } + ] + }, + { + name: 'readOnlyWithDefaults', + title: 'Read only with defaults', + description: 'This is read only', + type: 'array', + readOnly: true, + of: [ + {type: 'image', title: 'Image'}, + { + type: 'reference', + name: 'authorReference', + to: {type: 'author'}, + title: 'Reference to author' + }, + { + type: 'reference', + name: 'bookReference', + to: {type: 'book'}, + title: 'Reference to book' + }, + {type: 'author', title: 'Embedded author'}, + {type: 'code', title: 'Code'}, + { + type: 'color', + name: 'colorBlock', + title: 'Color (block)' + }, + { + type: 'object', + title: 'Test object', + name: 'testObject', + fields: [{name: 'field1', type: 'string'}] + }, + { + type: 'object', + title: 'Other test object', + name: 'otherTestObject', + fields: [ + {name: 'field1', type: 'string'}, + { + name: 'field3', + type: 'array', + of: [ + { + type: 'object', + fields: [{name: 'aString', type: 'string'}, {name: 'aNumber', type: 'number'}] + } + ] + } + ] + }, + { + type: 'block', + of: [ + { + type: 'color', + title: 'Color' + } + ] + } + ] + }, + { + name: 'minimal', + title: 'Reset all options', + type: 'array', + of: [ + { + type: 'block', + styles: [], + lists: [], + marks: { + decorators: [], + annotations: [] + } + } + ] + }, + { + name: 'customized', + title: 'Customized with block types', + type: 'array', + of: [ + {type: 'author', title: 'Author'}, + { + type: 'block', + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + {title: 'Quote', value: 'blockquote'} + ], + lists: [{title: 'Bullet', value: 'bullet'}, {title: 'Numbered', value: 'number'}], + marks: { + decorators: [{title: 'Strong', value: 'strong'}, {title: 'Emphasis', value: 'em'}], + annotations: [ + {name: 'Author', title: 'Author', type: 'reference', to: {type: 'author'}} + ] + }, + of: [ + { + type: 'image', + title: 'Image', + fields: [ + {title: 'Caption', name: 'caption', type: 'string', options: {isHighlighted: true}}, + { + title: 'Authors', + name: 'authors', + type: 'array', + options: {isHighlighted: true}, + of: [{type: 'author', title: 'Author'}] + } + ] + } + ] + } + ] + }, + { + name: 'deep', + title: 'Blocks deep down', + type: 'object', + fields: [ + {name: 'something', title: 'Something', type: 'string'}, + { + name: 'blocks', + type: 'array', + title: 'Blocks', + of: [ + {type: 'image', title: 'Image'}, + {type: 'author', title: 'Author'}, + { + type: 'block', + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + {title: 'Quote', value: 'blockquote'} + ], + lists: [{title: 'Bullet', value: 'bullet'}, {title: 'Numbered', value: 'number'}], + marks: { + decorators: [{title: 'Strong', value: 'strong'}, {title: 'Emphasis', value: 'em'}], + annotations: [ + {name: 'Author', title: 'Author', type: 'reference', to: {type: 'author'}} + ] + } + } + ] + } + ] + }, + { + title: 'Array of articles', + name: 'arrayOfArticles', + type: 'array', + of: [ + { + type: 'blocksTest' + } + ] + }, + { + title: 'Block in block', + name: 'blockInBlock', + type: 'array', + of: [ + { + type: 'block', + of: [ + { + name: 'footnote', + title: 'Footnote', + type: 'object', + fields: [ + { + title: 'Footnote', + name: 'footnote', + type: 'array', + of: [ + { + type: 'block', + lists: [], + styles: [], + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'} + ], + annotations: [] + } + } + ] + } + ] + } + ] + } + ] + }, + { + name: 'recursive', + type: 'object', + fields: [ + { + name: 'blocks', + type: 'array', + title: 'Blocks', + of: [ + { + type: 'block', + styles: [], + lists: [], + marks: { + decorators: [], + annotations: [] + } + }, + { + type: 'blocksTest', + title: 'Blocks test!' + } + ] + } + ] + }, + { + name: 'blockList', + title: 'Array of blocks', + type: 'array', + of: [ + { + name: 'blockListEntry', + type: 'object', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'blocks', + type: 'array', + of: [{type: 'block'}] + } + ] + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/book.js b/packages/backstop-test-studio/schemas/book.js new file mode 100644 index 00000000000..cbb7378e5f1 --- /dev/null +++ b/packages/backstop-test-studio/schemas/book.js @@ -0,0 +1,108 @@ +import BookIcon from 'react-icons/lib/fa/book' + +function formatSubtitle(book) { + return [ + 'By', + book.authorName || '', + book.authorBFF && `[BFF ${book.authorBFF} đŸ€ž]`, + book.publicationYear && `(${book.publicationYear})` + ] + .filter(Boolean) + .join(' ') +} + +export default { + name: 'book', + type: 'document', + title: 'Book', + description: 'This is just a simple type for generating some test data', + icon: BookIcon, + fields: [ + { + name: 'title', + title: 'Title', + type: 'string', + validation: Rule => Rule.min(5).max(100) + }, + { + name: 'translations', + title: 'Translations', + type: 'object', + fields: [ + {name: 'no', type: 'string', title: 'Norwegian (BokmĂ„l)'}, + {name: 'nn', type: 'string', title: 'Norwegian (Nynorsk)'}, + {name: 'se', type: 'string', title: 'Swedish'} + ] + }, + { + name: 'author', + title: 'Author', + type: 'reference', + to: {type: 'author', title: 'Author'} + }, + { + name: 'coverImage', + title: 'Cover Image', + type: 'image' + }, + { + name: 'publicationYear', + title: 'Year of publication', + type: 'number' + }, + { + name: 'isbn', + title: 'ISBN number', + description: 'ISBN-number of the book. Not shown in studio.', + type: 'number', + hidden: true + } + ], + orderings: [ + { + title: 'Title', + name: 'title', + by: [{field: 'title', direction: 'asc'}, {field: 'publicationYear', direction: 'asc'}] + }, + { + title: 'Author name', + name: 'authorName', + by: [{field: 'author.name', direction: 'asc'}] + }, + { + title: 'Authors best friend', + name: 'authorBFF', + by: [{field: 'author.bestFriend.name', direction: 'asc'}] + }, + { + title: 'Size of coverImage', + name: 'coverImageSize', + by: [{field: 'coverImage.asset.size', direction: 'asc'}] + }, + { + title: 'Swedish title', + name: 'swedishTitle', + by: [{field: 'translations.se', direction: 'asc'}, {field: 'title', direction: 'asc'}] + } + ], + preview: { + select: { + title: 'title', + translations: 'translations', + createdAt: '_createdAt', + date: '_updatedAt', + authorName: 'author.name', + authorBFF: 'author.bestFriend.name', + publicationYear: 'publicationYear', + media: 'coverImage' + }, + prepare(book, options = {}) { + return Object.assign({}, book, { + title: + ((options.ordering || {}).name === 'swedishTitle' && (book.translations || {}).se) || + book.title, + subtitle: formatSubtitle(book) + }) + } + } +} diff --git a/packages/backstop-test-studio/schemas/booleans.js b/packages/backstop-test-studio/schemas/booleans.js new file mode 100644 index 00000000000..d09dfe85719 --- /dev/null +++ b/packages/backstop-test-studio/schemas/booleans.js @@ -0,0 +1,45 @@ +import icon from 'react-icons/lib/md/check-box' + +export default { + name: 'booleansTest', + type: 'document', + title: 'Booleans test', + icon, + fieldsets: [ + { + name: 'collection', + title: 'Collection of switches', + description: 'Show how long descriptions behave on av switch in a fieldset' + } + ], + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'switch', + type: 'boolean', + description: 'Should be either true or false', + title: 'Check me?' + }, + { + name: 'checkbox', + type: 'boolean', + description: 'Should be displayed as a checkbox', + options: { + layout: 'checkbox' + }, + title: 'Checked?' + }, + { + name: 'switchLong', + type: 'boolean', + title: 'A switch with a long description', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean convallis suscipit diam, eget blandit orci euismod in. Curabitur ac tellus pellentesque, porttitor tellus id, venenatis massa. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent eu augue mattis, accumsan risus ut, iaculis risus. Integer cursus justo nibh, ac ultricies orci lobortis et. Curabitur ac commodo justo, sit amet facilisis mi. Nunc arcu nibh, commodo maximus risus non, suscipit laoreet nisl. Nam dignissim, sem vel tempor tristique, metus odio vehicula sapien, nec rhoncus urna diam eget nisl. In ut arcu ante. Pellentesque maximus, mi non faucibus hendrerit, massa massa pellentesque arcu, ac egestas odio nunc ac erat.', + fieldset: 'collection' + } + ] +} diff --git a/packages/backstop-test-studio/schemas/code.js b/packages/backstop-test-studio/schemas/code.js new file mode 100644 index 00000000000..1192d63fac8 --- /dev/null +++ b/packages/backstop-test-studio/schemas/code.js @@ -0,0 +1,51 @@ +import icon from 'react-icons/lib/md/code' + +export default { + name: 'codeTest', + type: 'document', + title: 'Code test', + icon, + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'code', + title: 'Default theme', + description: 'Selectable language', + type: 'code', + options: { + languageAlternatives: [ + {title: 'LaTeX', value: 'latex'}, + {title: 'JavaScript', value: 'javascript'}, + {title: 'CSS', value: 'css'}, + {title: 'text', value: 'text'} + ] + } + }, + { + name: 'cssOrJsCode', + title: 'Github theme', + type: 'code', + options: { + theme: 'github', + languageAlternatives: [ + {title: 'JavaScript', value: 'javascript'}, + {title: 'CSS', value: 'css'} + ] + } + }, + { + name: 'jscode', + title: 'Monokai theme', + description: 'Only javascript', + type: 'code', + options: { + theme: 'monokai', + language: 'javascript' + } + } + ] +} diff --git a/packages/backstop-test-studio/schemas/color.js b/packages/backstop-test-studio/schemas/color.js new file mode 100644 index 00000000000..a219c246d5f --- /dev/null +++ b/packages/backstop-test-studio/schemas/color.js @@ -0,0 +1,117 @@ +/* eslint-disable react/display-name */ +import React from 'react' +import icon from 'react-icons/lib/md/format-color-fill' + +export default { + name: 'colorTest', + type: 'document', + title: 'Color', + icon, + preview: { + select: { + title: 'title', + color: 'testColor1' + }, + + prepare({title, color}) { + let subtitle = (color && color.hex) || 'No color set' + if (color && color.hsl) { + subtitle = `${color.hex}` //eslint-disable-line max-len + } + return { + title: title, + subtitle: subtitle, + description: + color && + color.hsl && + `H:${Math.round(color.hsl.l * 100)} S:${Math.round(color.hsl.l * 100)} L:${Math.round( + color.hsl.l * 100 + )} A:${Math.round(color.hsl.a * 100)}`, + media: () => ( +
+ ) + } + } + }, + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'testColor1', + title: 'Color to be used in preview', + description: 'A color input', + type: 'color' + }, + { + name: 'testColor2', + title: 'Color with no alpha', + description: 'A color input with no alpha', + type: 'color', + options: { + disableAlpha: true + } + }, + { + name: 'colorList', + title: 'List of colors', + description: 'An array of colors with the built in color preview', + type: 'array', + of: [ + { + type: 'color' + } + ] + }, + { + name: 'readOnlyColor', + title: 'Read-only color', + description: 'Color input in readOnly mode', + readOnly: true, + type: 'color' + }, + { + name: 'colorGrid', + title: 'Grid of colors', + description: 'An grid of colors with the built in color preview', + type: 'array', + options: { + layout: 'grid' + }, + of: [ + { + type: 'color' + } + ] + }, + { + name: 'objectWithObjectWithColors', + title: 'Object with object with colors', + type: 'object', + fields: [ + { + name: 'objectWithColors', + title: 'Object with colors', + type: 'object', + fields: [ + {name: 'primaryColor', title: 'Primary color', type: 'color'}, + {name: 'secondaryColor', title: 'Secondary color', type: 'color'}, + {name: 'extraColor', title: 'Extra color', type: 'color'} + ] + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/date.js b/packages/backstop-test-studio/schemas/date.js new file mode 100644 index 00000000000..aee4e06c874 --- /dev/null +++ b/packages/backstop-test-studio/schemas/date.js @@ -0,0 +1,67 @@ +import icon from 'react-icons/lib/fa/calendar' + +export default { + name: 'dateTest', + type: 'document', + title: 'Date test', + icon, + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'justDefaults', + type: 'date', + title: 'Datetime with default config' + }, + { + name: 'aDateWithCustomDateFormat', + type: 'date', + title: 'A date field with custom date format', + options: { + dateFormat: 'Do. MMMM YYYY' + } + }, + { + name: 'justARegularStringFieldInBetween', + type: 'string', + title: 'Some string', + description: 'A string field in between' + }, + { + name: 'aDateWithDefaults', + type: 'date', + title: 'A date field with defaults' + }, + { + name: 'aReadOnlyDate', + type: 'date', + title: 'A read only date', + readOnly: true + }, + { + name: 'customPlaceholder', + type: 'date', + title: 'Date without custom placeholder', + placeholder: 'Enter a date here' + }, + { + name: 'inArray', + type: 'array', + of: [ + { + type: 'object', + fields: [ + { + name: 'date', + type: 'date', + title: 'A date field in an array' + } + ] + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/datetime.js b/packages/backstop-test-studio/schemas/datetime.js new file mode 100644 index 00000000000..c02043dcbe8 --- /dev/null +++ b/packages/backstop-test-studio/schemas/datetime.js @@ -0,0 +1,87 @@ +import icon from 'react-icons/lib/fa/calendar' + +export default { + name: 'datetimeTest', + type: 'document', + title: 'Datetime test', + icon, + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'justDefaults', + type: 'datetime', + title: 'Datetime with default config' + }, + { + name: 'aDateTimeWithCustomDateFormat', + type: 'datetime', + title: 'A datetime field with custom date format', + options: { + dateFormat: 'Do. MMMM YYYY' + } + }, + { + name: 'justARegularStringFieldInBetween', + type: 'string', + title: 'Some string', + description: 'A string field in between' + }, + { + name: 'aDateTimeWithCustomTimeFormat', + type: 'datetime', + title: 'A datetime field with custom time format', + options: { + timeFormat: 'hh:mm' + } + }, + { + name: 'aReadOnlyDateTime', + type: 'datetime', + title: 'A read only datetime', + readOnly: true + }, + { + name: 'aDateTimeWithCustomDateAndTimeFormat', + type: 'datetime', + title: 'A datetime field with a custom date AND time format', + options: { + dateFormat: 'Do. MMMM YYYY', + timeFormat: 'hh:mm' + } + }, + { + name: 'aDateFieldWithTimeStep', + type: 'datetime', + title: 'A date field with timeStep', + options: { + timeStep: 30 + } + }, + { + name: 'customPlaceholder', + type: 'datetime', + title: 'Datetime without custom placeholder', + placeholder: 'Enter a date here' + }, + { + name: 'inArray', + type: 'array', + of: [ + { + type: 'object', + fields: [ + { + name: 'date', + type: 'datetime', + title: 'A datetime field with custom date format' + } + ] + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/emails.js b/packages/backstop-test-studio/schemas/emails.js new file mode 100644 index 00000000000..e3b476bde38 --- /dev/null +++ b/packages/backstop-test-studio/schemas/emails.js @@ -0,0 +1,21 @@ +import icon from 'react-icons/lib/md/email' + +export default { + name: 'emailsTest', + type: 'document', + title: 'Emails test', + icon, + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'myUrlField', + type: 'email', + title: 'Plain email field', + description: 'A plain email field' + } + ] +} diff --git a/packages/backstop-test-studio/schemas/empty.js b/packages/backstop-test-studio/schemas/empty.js new file mode 100644 index 00000000000..e4999db2be5 --- /dev/null +++ b/packages/backstop-test-studio/schemas/empty.js @@ -0,0 +1,13 @@ +export default { + name: 'empty', + type: 'document', + title: 'Empty', + description: 'Do not put content here', + fields: [ + { + name: 'name', + title: 'Name', + type: 'string' + } + ] +} diff --git a/packages/backstop-test-studio/schemas/fieldsets.js b/packages/backstop-test-studio/schemas/fieldsets.js new file mode 100644 index 00000000000..3d6dd332941 --- /dev/null +++ b/packages/backstop-test-studio/schemas/fieldsets.js @@ -0,0 +1,80 @@ +import icon from 'react-icons/lib/fa/tasks' + +export default { + name: 'fieldsetsTest', + type: 'document', + title: 'Fieldsets test', + icon, + preview: { + select: { + title: 'myObject.first' + } + }, + fieldsets: [{name: 'recursive', title: 'Recursive', options: {collapsable: true}}], + fields: [ + { + name: 'myObject', + type: 'myObject', + title: 'MyObject', + description: 'The first field here should be the title' + }, + { + name: 'person', + type: 'object', + fieldsets: [ + { + name: 'social', + title: 'Social media handles [collapsed by default]', + options: {collapsible: true, collapsed: true} + } + ], + fields: [ + { + name: 'name', + title: 'Name', + type: 'string' + }, + { + name: 'twitter', + title: 'Twitter', + type: 'string', + fieldset: 'social' + }, + { + name: 'instagram', + title: 'Instagram', + type: 'string', + fieldset: 'social' + }, + { + name: 'facebook', + title: 'Facebook', + type: 'string', + fieldset: 'social' + } + ] + }, + { + name: 'fieldWithObjectType', + title: 'Field of object type', + type: 'object', + description: + 'This is a field of (anonymous, inline) object type. Values here should never get a `_type` property', + fields: [ + {name: 'field1', type: 'string', description: 'This is a string field'}, + { + name: 'field2', + type: 'myObject', + title: 'A field of myObject', + description: 'This is another field of "myObject"' + } + ] + }, + { + name: 'recursive', + title: 'This field is of type objectsTest', + type: 'objectsTest', + fieldset: 'recursive' + } + ] +} diff --git a/packages/backstop-test-studio/schemas/geopoint.js b/packages/backstop-test-studio/schemas/geopoint.js new file mode 100644 index 00000000000..ed289a5c0ec --- /dev/null +++ b/packages/backstop-test-studio/schemas/geopoint.js @@ -0,0 +1,62 @@ +import React from 'react' +import icon from 'react-icons/lib/md/pin-drop' +import config from 'config:@sanity/google-maps-input' + +export default { + name: 'geopointTest', + type: 'document', + title: 'Geopoint test', + icon, + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'location', + type: 'geopoint', + title: 'A geopoint', + description: 'This is a geopoint field' + }, + { + name: 'arrayOfLocations', + type: 'array', + of: [ + { + type: 'geopoint', + title: 'A geopoint', + description: 'This is a geopoint field' + } + ] + } + ], + preview: { + select: { + title: 'title', + location: 'location' + }, + prepare({title, location}) { + const {apiKey} = config + return { + title: title, + subtitle: + location && + `${Number(location.lat).toPrecision(5)}, ${Number(location.lng).toPrecision(5)}`, + media({dimensions}) { + if (!location || !location.lat || !location.lng || !apiKey) { + return null + } + return ( + {title} + ) + } + } + } + } +} diff --git a/packages/backstop-test-studio/schemas/images.js b/packages/backstop-test-studio/schemas/images.js new file mode 100644 index 00000000000..ec8067ede7c --- /dev/null +++ b/packages/backstop-test-studio/schemas/images.js @@ -0,0 +1,136 @@ +import icon from 'react-icons/lib/md/photo-library' + +export const myImage = { + name: 'myImage', + title: 'Some image type', + type: 'image', + fields: [ + { + name: 'caption', + title: 'Caption', + type: 'string' + } + ] +} + +export default { + name: 'imagesTest', + type: 'document', + title: 'Images test', + icon, + description: 'Different test cases of image fields', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'mainImage', + title: 'Image', + type: 'image', + description: + 'Image hotspot should be possible to change. Caption should be visible in image field, full description should be editable in modal', + options: { + hotspot: true + }, + fields: [ + { + name: 'caption', + type: 'string', + title: 'Caption', + options: { + isHighlighted: true + } + }, + { + name: 'foo', + type: 'string', + title: + 'This is a rather longish title for a field. It should still work. This is a rather longish title for a field. It should still work.', + options: { + isHighlighted: true + } + }, + { + name: 'description', + type: 'string', + title: 'Full description' + } + ] + }, + { + name: 'myImage', + title: 'Field of custom image type', + type: 'myImage', + description: 'Should be like myImage', + required: true + }, + { + name: 'pngImage', + title: 'PNG image', + type: 'image', + description: 'Should not accept other image types than png', + options: { + hotspot: true, + accept: 'image/png' + } + }, + { + name: 'jpegImageWithLqip', + title: 'JPEG image', + type: 'image', + description: 'Should only accept JPEGs and will extract only LQIP and location metadata', + options: { + accept: 'image/jpeg', + metadata: ['location', 'lqip'] + } + }, + { + name: 'imageWithImage', + title: 'Image with image', + type: 'image', + description: 'This is a weird example of an image that has an image as one of its fields', + options: { + hotspot: true + }, + fields: [ + { + name: 'caption', + type: 'string', + title: 'Caption', + options: { + isHighlighted: true + } + }, + { + name: 'foo', + type: 'string', + title: + 'This is a rather longish title for a field. It should still work. This is a rather longish title for a field. It should still work.', + options: { + isHighlighted: true + } + }, + { + name: 'image', + type: 'image', + title: 'Image in image', + options: { + isHighlighted: true + } + }, + { + name: 'description', + type: 'string', + title: 'Full description' + }, + { + name: 'extraImage', + type: 'image', + title: 'Image in image behind edit' + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/invalidPreviews.js b/packages/backstop-test-studio/schemas/invalidPreviews.js new file mode 100644 index 00000000000..689d59ef460 --- /dev/null +++ b/packages/backstop-test-studio/schemas/invalidPreviews.js @@ -0,0 +1,76 @@ +const rnd = Math.random() + +export default { + name: 'invalidPreviews', + type: 'document', + title: 'Preview: Invalid preview configs', + preview: { + select: { + title: 'title', + media: 'array' + }, + prepare(invalue) { + if (rnd < 0.2) { + throw new Error('nope') + } + if (rnd < 0.4) { + return null + } + if (rnd < 0.6) { + return 'WILLNOTWORK' + } + if (rnd < 0.8) { + return {title: new Date()} + } + return invalue + } + }, + fields: [ + { + name: 'title', + title: 'Title', + type: 'object', + fields: [ + {name: 'en', type: 'string', title: 'English'}, + {name: 'no', type: 'string', title: 'Norwegian'} + ] + }, + { + name: 'image', + title: 'Image', + type: 'image' + }, + { + name: 'array', + type: 'array', + title: 'Array', + of: [ + { + type: 'object', + name: 'customObjectWithInvalidPreview', + fields: [ + { + name: 'objectWithInvalidPreview', + title: 'Title', + type: 'object', + fields: [ + { + name: 'someObj', + type: 'object', + title: 'Object', + fields: [{name: 'someString', type: 'string'}] + } + ], + preview: { + select: {title: 'objectWithInvalidPreview'} + } + } + ], + preview: { + select: {title: 'objectWithInvalidPreview'} + } + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/notitle.js b/packages/backstop-test-studio/schemas/notitle.js new file mode 100644 index 00000000000..4652dbd07aa --- /dev/null +++ b/packages/backstop-test-studio/schemas/notitle.js @@ -0,0 +1,37 @@ +// Example type that has no obvious candidate fields for sort or preview +export default { + name: 'noTitleField', + type: 'document', + title: 'No title field', + fields: [ + { + name: 'isChecked', + type: 'boolean', + title: 'Is checked?' + }, + { + name: 'somethings', + title: 'Some things', + type: 'array', + of: [{type: 'string'}] + }, + { + name: 'something', + type: 'document', + title: 'Some thing', + fields: [{name: 'first', type: 'string'}, {name: 'second', type: 'string'}] + }, + { + name: 'arrayOfBooks', + type: 'array', + title: 'Array of books', + of: [{type: 'book', title: 'Book'}] + }, + { + name: 'arrayOfMyself', + type: 'array', + title: 'Array of my own type', + of: [{type: 'noTitleField', title: 'My own type'}] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/objects.js b/packages/backstop-test-studio/schemas/objects.js new file mode 100644 index 00000000000..dc3fca53dac --- /dev/null +++ b/packages/backstop-test-studio/schemas/objects.js @@ -0,0 +1,111 @@ +import icon from 'react-icons/lib/go/puzzle' + +export const myObject = { + type: 'object', + name: 'myObject', + title: 'My object', + icon, + fields: [ + { + name: 'first', + type: 'string', + title: 'First' + }, + { + name: 'second', + type: 'string', + title: 'Second' + } + ] +} + +export default { + name: 'objectsTest', + type: 'document', + title: 'Objects test', + preview: { + select: { + title: 'myObject.first' + } + }, + fieldsets: [{name: 'recursive', title: 'Recursive', options: {collapsable: true}}], + fields: [ + { + name: 'myObject', + type: 'myObject', + title: 'MyObject', + description: 'The first field here should be the title' + }, + { + name: 'fieldWithObjectType', + title: 'Field of object type', + type: 'object', + description: + 'This is a field of (anonymous, inline) object type. Values here should never get a `_type` property', + fields: [ + {name: 'field1', type: 'string', description: 'This is a string field'}, + { + name: 'field2', + type: 'myObject', + title: 'A field of myObject', + description: 'This is another field of "myObject"' + } + ] + }, + { + name: 'recursive', + title: 'This field is of type objectsTest', + type: 'objectsTest', + fieldset: 'recursive' + }, + { + name: 'collapsibleObject', + title: 'Collapsible object', + type: 'object', + options: {collapsible: true, collapsed: false}, + description: + 'This is a field of (anonymous, inline) object type. Values here should never get a `_type` property', + fields: [ + {name: 'field1', type: 'string', description: 'This is a string field'}, + {name: 'field2', type: 'string', description: 'This is a collapsed field'} + ] + }, + { + name: 'events', + title: 'Events', + type: 'array', + of: [ + { + name: 'mbwEvent', + type: 'object', + preview: { + prepare({where, what}) { + return { + title: where, + subtitle: (what || []).join(', '), + media: () => (where || '').slice(0, 1) + } + } + }, + fields: [ + { + name: 'where', + title: 'Where', + description: 'Victoriagade? Baghaven? Koelschip?', + type: 'string', + validation: Rule => Rule.required() + }, + { + name: 'what', + title: 'What', + description: 'Party? Bottle release? Tap takeover?', + type: 'array', + of: [{type: 'string'}], + validation: Rule => Rule.min(1) + } + ] + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/readOnly.js b/packages/backstop-test-studio/schemas/readOnly.js new file mode 100644 index 00000000000..3166bee83fb --- /dev/null +++ b/packages/backstop-test-studio/schemas/readOnly.js @@ -0,0 +1,280 @@ +import icon from 'react-icons/lib/md/text-format' +import React from 'react' + +export default { + name: 'readOnlyTest', + type: 'document', + title: 'Read only test', + icon, + fields: [ + { + name: 'title', + type: 'string', + title: 'Title', + description: 'This is a basic string field', + readOnly: true + }, + { + name: 'select', + type: 'string', + title: 'Select string', + description: + 'Select a single string value from a set of predefined options. It should be possible to unset a selected value.', + readOnly: true, + options: { + list: [ + { + title: 'One (1)', + value: 'one' + }, + { + title: 'Two (2)', + value: 'two' + }, + { + title: 'Three (3)', + value: 'three' + } + ] + } + }, + { + name: 'radioSelectHorizontal', + title: 'Select (layout: radio, direction: horizontal)', + type: 'string', + description: + 'Select a single string value by choosing options from a list of radio buttons. It should *not* be possible to unset a selected value once its set.', + readOnly: true, + options: { + layout: 'radio', + direction: 'horizontal', + list: [ + { + title: 'One (1)', + value: 'one' + }, + { + title: 'Two (2)', + value: 'two' + }, + { + title: 'Three (3)', + value: 'three' + } + ] + } + }, + { + name: 'selectObjectOfString', + title: 'Select string in object', + description: + 'Select a single string value from an array of strings. It should be possible to unset a selected value.', + type: 'string', + readOnly: true, + options: { + list: ['one', 'two', 'three'] + } + }, + { + title: 'Reference to book or author', + name: 'multiTypeRef', + type: 'reference', + readOnly: true, + to: [{type: 'book'}, {type: 'author'}] + }, + { + name: 'anObject', + type: 'object', + title: 'A read only object', + readOnly: true, + fields: [{name: 'first', type: 'string'}, {name: 'second', type: 'string'}] + }, + { + name: 'myObject', + type: 'myObject', + title: 'MyObject', + description: 'The first field here should be the title', + readOnly: true + }, + { + title: 'Description', + name: 'description', + type: 'text', + readOnly: true + }, + { + title: 'Number', + name: 'popularity', + type: 'number', + readOnly: true + }, + { + title: 'Has the movie been released?', + name: 'released', + type: 'boolean', + readOnly: true + }, + { + title: 'Has the movie been released?', + name: 'releasedCheckbox', + type: 'boolean', + readOnly: true, + options: { + layout: 'checkbox' + } + }, + { + name: 'date', + title: 'Date', + description: 'A read only date', + type: 'datetime', + readOnly: true + }, + { + name: 'tags', + title: 'Tags', + description: + 'Enter a tag and press enter. Should result in an array of strings and should be possible to remove items', + type: 'array', + options: {layout: 'tags'}, + of: [{type: 'string'}], + readOnly: true + }, + { + name: 'readOnlyArray', + title: 'Polymorphic grid array', + description: 'An array of multiple types. options: {layout: "grid"}', + type: 'array', + readOnly: true, + of: [ + { + title: 'A book', + type: 'book' + } + ] + }, + { + name: 'readOnlyImage', + title: 'Read only image field', + type: 'image', + readOnly: true, + options: {hotspot: true}, + fields: [ + { + title: 'Caption', + type: 'string', + name: 'caption', + options: {isHighlighted: true} + }, + { + title: 'Attribution', + name: 'attribution', + type: 'string', + options: {isHighlighted: true} + }, + { + title: 'Extra', + name: 'extra', + type: 'string' + } + ] + }, + { + name: 'readOnlyFile', + title: 'Read only file field', + type: 'file', + readOnly: true, + fields: [ + { + title: 'Caption', + type: 'string', + name: 'caption', + options: {isHighlighted: true} + }, + { + title: 'Attribution', + name: 'attribution', + type: 'string', + options: {isHighlighted: true} + }, + { + title: 'Extra', + name: 'extra', + type: 'string' + } + ] + }, + { + name: 'readOnlyArrayOfPrimitives', + title: 'Array with primitive types', + description: 'This array contains only strings, values and booleans', + type: 'array', + readOnly: true, + of: [ + { + type: 'string', + title: 'String' + }, + { + type: 'number', + title: 'Number' + }, + { + type: 'boolean', + title: 'Boolean' + } + ] + }, + { + name: 'readOnlyArrayOfPredefinedOptions', + title: 'Read only array of predefined options', + type: 'array', + readOnly: true, + of: [ + { + type: 'object', + name: 'color', + fields: [ + { + name: 'title', + type: 'string' + }, + { + name: 'name', + type: 'string' + } + ], + preview: { + select: { + title: 'title', + name: 'name' + }, + prepare({title, name}) { + return { + title: title, + media: () => ( +
+ ) + } + } + } + } + ], + options: { + direction: 'vertical', + list: [ + {_type: 'color', title: 'Red', name: 'red', _key: 'red'}, + {_type: 'color', title: 'Green', name: 'green', _key: 'green'}, + {_type: 'color', title: 'Blue', name: 'blue', _key: 'blue'}, + {_type: 'color', title: 'Black', name: 'black', _key: 'black'} + ] + } + } + ] +} diff --git a/packages/backstop-test-studio/schemas/references.js b/packages/backstop-test-studio/schemas/references.js new file mode 100644 index 00000000000..4f26df8d3a0 --- /dev/null +++ b/packages/backstop-test-studio/schemas/references.js @@ -0,0 +1,62 @@ +import icon from 'react-icons/lib/fa/recycle' + +export default { + name: 'referenceTest', + type: 'document', + title: 'Reference test', + description: 'Test cases for references', + icon, + fields: [ + {name: 'title', type: 'string'}, + {name: 'selfRef', type: 'reference', to: {type: 'referenceTest'}}, + { + name: 'refToTypeWithNoToplevelStrings', + type: 'reference', + to: {type: 'typeWithNoToplevelStrings'} + }, + { + name: 'someWeakRef', + type: 'reference', + weak: true, + to: {type: 'author'} + }, + { + title: 'Reference to book or author', + name: 'multiTypeRef', + type: 'reference', + to: [{type: 'book'}, {type: 'author'}] + }, + { + name: 'array', + type: 'array', + of: [ + { + type: 'reference', + name: 'strongAuthorRef', + title: 'A strong author ref', + to: {type: 'author'} + }, + { + type: 'reference', + name: 'weakAuthorRef', + title: 'A weak author ref', + weak: true, + to: {type: 'author'} + } + ] + } + ], + preview: { + fields: { + title: 'title', + author0: 'array.0.name', + author1: 'array.1.name' + }, + prepare(val) { + return { + title: val.title, + subtitle: [val.author0, val.author1].filter(Boolean).join(', ') + } + } + } +} diff --git a/packages/backstop-test-studio/schemas/schema.js b/packages/backstop-test-studio/schemas/schema.js new file mode 100644 index 00000000000..a6793b0c62c --- /dev/null +++ b/packages/backstop-test-studio/schemas/schema.js @@ -0,0 +1,53 @@ +import createSchema from 'part:@sanity/base/schema-creator' +import schemaTypes from 'all:part:@sanity/base/schema-type' + +import arrays, {topLevelArrayType, topLevelPrimitiveArrayType} from './arrays' +import blocks from './blocks' +import code from './code' +import date from './date' +import color from './color' +import booleans from './booleans' +import texts from './texts' +import book from './book' +import author from './author' +import images, {myImage} from './images' +import objects, {myObject} from './objects' +import species from './species' +import readOnly from './readOnly' +import notitle from './notitle' +import empty from './empty' +import invalidPreviews from './invalidPreviews' +import validation from './validation' +import references from './references' +import geopoint from './geopoint' +import typeWithNoToplevelStrings from './typeWithNoToplevelStrings' + +export default createSchema({ + name: 'test-examples', + types: schemaTypes.concat([ + book, + author, + arrays, + code, + color, + blocks, + date, + texts, + species, + images, + readOnly, + references, + geopoint, + notitle, + empty, + myImage, + objects, + myObject, + booleans, + validation, + topLevelArrayType, + topLevelPrimitiveArrayType, + invalidPreviews, + typeWithNoToplevelStrings + ]) +}) diff --git a/packages/backstop-test-studio/schemas/slugs.js b/packages/backstop-test-studio/schemas/slugs.js new file mode 100644 index 00000000000..a0e7f7d595e --- /dev/null +++ b/packages/backstop-test-studio/schemas/slugs.js @@ -0,0 +1,120 @@ +export default { + name: 'slugsTest', + type: 'document', + title: 'Slugs test', + preview: { + select: { + title: 'title', + subtitle: 'slug.current' + }, + prepare: ({title, subtitle}) => { + return { + title: title, + subtitle: `/${subtitle}/` + } + } + }, + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'slug', + type: 'slug', + title: 'Normal slug', + description: 'This is a slug field that should update according to current title', + options: { + source: 'title', + maxLength: 96 + } + }, + { + name: 'requiredSlug', + type: 'slug', + title: 'Required slug', + description: 'Slug field that is required', + validation: Rule => Rule.required(), + options: { + source: 'title', + maxLength: 96 + } + }, + { + name: 'slugWithFunction', + type: 'slug', + title: 'Slug with function to get source', + description: 'This is a slug field that should update according to current title', + options: { + source: document => document.title, + maxLength: 96 + } + }, + { + name: 'slugWithCustomUniqueCheck', + type: 'slug', + title: 'Slug with custom unique check', + description: 'Slugs starting with "hei" are always taken, regardless of documents using it', + options: { + source: 'title', + maxLength: 100, + isUnique: (value, options) => + !/^hei/i.test(value) && options.defaultIsUnique(value, options) + } + }, + { + name: 'arrayOfSlugs', + type: 'array', + of: [ + { + options: { + source: 'title' + }, + type: 'slug' + } + ] + }, + { + name: 'experiments', + title: 'Experiments', + type: 'array', + of: [{type: 'experiment'}] + }, + { + name: 'deprecatedSlugifyFnField', + type: 'slug', + title: 'Slug field using deprecated "slugifyFn" option', + description: 'Should warn the developer about deprecated field', + options: { + source: 'title', + slugifyFn: value => + value + .toLocaleLowerCase() + .split('') + .reverse() + .join('') + .replace(/\s+/g, '-') + } + }, + { + name: 'nested', + type: 'object', + fields: [ + { + name: 'slugWithSlugify', + type: 'slug', + title: 'Custom slugify function', + description: 'This is a slug field that should update according to current title', + options: { + source: 'title', + maxLength: 96, + slugify: (value, type) => { + return encodeURI(`${type.name}_${value}`).toLocaleLowerCase() + } + } + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/schemas/species.js b/packages/backstop-test-studio/schemas/species.js new file mode 100644 index 00000000000..b7c6dee4c41 --- /dev/null +++ b/packages/backstop-test-studio/schemas/species.js @@ -0,0 +1,37 @@ +import React from 'react' + +export default { + name: 'species', + title: 'Species', + type: 'document', + fields: [ + { + name: 'name', + title: 'Common name', + type: 'string' + }, + { + name: 'genus', + title: 'Genus', + type: 'string' + }, + { + name: 'species', + title: 'Species', + type: 'string' + } + ], + preview: { + select: { + name: 'name', + genus: 'genus', + species: 'species' + }, + prepare(value) { + return { + title: `${value.genus} ${value.species}`, + subtitle: value.name + } + } + } +} diff --git a/packages/backstop-test-studio/schemas/strings.js b/packages/backstop-test-studio/schemas/strings.js new file mode 100644 index 00000000000..36081031a68 --- /dev/null +++ b/packages/backstop-test-studio/schemas/strings.js @@ -0,0 +1,116 @@ +import icon from 'react-icons/lib/md/text-format' + +export default { + name: 'stringsTest', + type: 'document', + title: 'Strings test', + icon, + fields: [ + { + name: 'title', + type: 'string', + title: 'Title', + description: 'This is a basic string field' + }, + { + name: 'readonlyField', + type: 'string', + title: 'A read only string', + description: 'It may have a value, but it cannot be edited', + readOnly: true + }, + { + name: 'select', + type: 'string', + title: 'Select string', + description: + 'Select a single string value from a set of predefined options. It should be possible to unset a selected value.', + options: { + list: [ + { + title: 'One (1)', + value: 'one' + }, + { + title: 'Two (2)', + value: 'two' + }, + { + title: 'Three (3)', + value: 'three' + } + ] + } + }, + { + name: 'radioSelectHorizontal', + title: 'Select (layout: radio, direction: horizontal)', + type: 'string', + description: + 'Select a single string value by choosing options from a list of radio buttons. It should *not* be possible to unset a selected value once its set.', + options: { + layout: 'radio', + direction: 'horizontal', + list: [ + { + title: 'One (1)', + value: 'one' + }, + { + title: 'Two (2)', + value: 'two' + }, + { + title: 'Three (3)', + value: 'three' + } + ] + } + }, + { + name: 'selectObjectOfString', + title: 'Select string in object', + description: + 'Select a single string value from an array of strings. It should be possible to unset a selected value.', + type: 'string', + options: { + list: ['one', 'two', 'three'] + } + }, + { + name: 'radioSelect', + title: 'Select (layout: radio)', + type: 'string', + description: + 'Select a single string value by choosing options from a list of radio buttons. It should *not* be possible to unset a selected value once its set.', + options: { + layout: 'radio', + direction: 'vertical', + list: [ + { + title: 'One (1)', + value: 'one' + }, + { + title: 'Two (2)', + value: 'two' + }, + { + title: 'Three (3)', + value: 'three' + } + ] + } + }, + { + name: 'localeTitle', + title: 'Localized title', + type: 'localeString', + description: 'English required by localeString, swedish from field', + validation: Rule => + Rule.fields({ + se: fieldRule => fieldRule.required().max(80) + }) + } + ] +} diff --git a/packages/backstop-test-studio/schemas/texts.js b/packages/backstop-test-studio/schemas/texts.js new file mode 100644 index 00000000000..37bfecb96b0 --- /dev/null +++ b/packages/backstop-test-studio/schemas/texts.js @@ -0,0 +1,37 @@ +import icon from 'react-icons/lib/md/format-align-left' + +export default { + name: 'textsTest', + type: 'document', + title: 'Texts tests', + icon, + fields: [ + { + name: 'title', + type: 'text', + title: 'Simple text', + description: 'This is a simple text field' + }, + { + name: 'rows2', + type: 'text', + title: '2 rows text', + description: 'This is a simple text field', + rows: 2 + }, + { + name: 'rows15', + type: 'text', + title: '15 rows text', + description: 'This is a simple text field', + rows: 15 + }, + { + name: 'readonlyField', + type: 'text', + title: 'A read only text', + description: 'It may have a value, but it cannot be edited', + readOnly: true + } + ] +} diff --git a/packages/backstop-test-studio/schemas/typeWithNoToplevelStrings.js b/packages/backstop-test-studio/schemas/typeWithNoToplevelStrings.js new file mode 100644 index 00000000000..a56ef822ee2 --- /dev/null +++ b/packages/backstop-test-studio/schemas/typeWithNoToplevelStrings.js @@ -0,0 +1,36 @@ +export default { + name: 'typeWithNoToplevelStrings', + type: 'document', + title: 'Type without strings', + description: `This is an example of a type that has no meaningful top level strings. + It is used to test/demonstrate that reference search also includes deeper fields`, + fields: [ + { + name: 'localizedTitle', + title: 'Localized title', + type: 'object', + fieldsets: [{name: 'other', title: 'Translations'}], + fields: [ + {name: 'no', type: 'string', title: 'Norwegian (BokmÄl)'}, + {name: 'nn', type: 'string', title: 'Norwegian (Nynorsk)', fieldset: 'other'}, + {name: 'se', type: 'string', title: 'Swedish', fieldset: 'other'} + ] + }, + { + name: 'name', + title: "Name field (can't be sorted)", + type: 'object', + fieldsets: [{name: 'other', title: 'Translations'}], + fields: [ + {name: 'no', type: 'string', title: 'Norwegian (BokmÄl)'}, + {name: 'nn', type: 'string', title: 'Norwegian (Nynorsk)', fieldset: 'other'}, + {name: 'se', type: 'string', title: 'Swedish', fieldset: 'other'} + ] + }, + { + name: 'externalId', + title: 'External id', + type: 'string' + } + ] +} diff --git a/packages/backstop-test-studio/schemas/validation.js b/packages/backstop-test-studio/schemas/validation.js new file mode 100644 index 00000000000..56ec18cafce --- /dev/null +++ b/packages/backstop-test-studio/schemas/validation.js @@ -0,0 +1,415 @@ +import client from 'part:@sanity/base/client' +import {points, featureCollection} from '@turf/helpers' +import pointsWithinPolygon from '@turf/points-within-polygon' +import norway from '../data/norway' + +export default { + name: 'validationTest', + type: 'document', + title: 'Validation test', + validation: Rule => + Rule.custom(doc => { + if (!doc || !doc.title) { + return true + } + + const needsUrl = (doc.title[0] || '').toUpperCase() === doc.title[0] + return needsUrl && !doc.myUrlField + ? 'When the first character of the title is uppercase, you will need to fill out the "Plain url"-field' + : true + }), + fields: [ + { + name: 'title', + type: 'string', + title: 'Title', + description: 'Required field with minimum/maximum length validation', + validation: Rule => + Rule.required() + .min(5) + .max(100) + }, + { + name: 'slug', + type: 'slug', + title: 'Field of slug type', + description: 'Required, updates from title', + validation: Rule => Rule.required(), + options: { + source: document => document.title + } + }, + { + name: 'myUrlField', + type: 'url', + title: 'Plain url', + description: 'URL validation (inferred)' + }, + { + name: 'myFancyUrlField', + type: 'url', + title: 'Fancy URL', + description: 'URL that only allows mailto: and tel: schemes', + validation: Rule => Rule.uri({scheme: ['mailto', 'tel']}) + }, + { + name: 'date', + type: 'datetime', + title: 'Some date', + description: 'ISO-formatted date, inferred, must be in 2017', + validation: Rule => Rule.min('2017-01-01 00:00:00').max('2017-12-31 00:00:00') + }, + { + name: 'email', + type: 'email', + title: 'Recipient email', + description: 'Email, inferred' + }, + { + name: 'intro', + type: 'text', + title: 'Introduction', + description: 'Required, and warn if less than 50 characters', + validation: Rule => [ + Rule.required(), + Rule.min(50).warning('Should probably be at least 50 characters') + ] + }, + { + name: 'lowestTemperature', + type: 'number', + title: 'Lowest temperature recorded', + description: 'Only negative numbers', + validation: Rule => Rule.negative() + }, + { + name: 'highestTemperature', + type: 'number', + title: 'Highest temperature recorded', + description: 'Only positive numbers larger than lowest temperature', + validation: Rule => Rule.positive().min(Rule.valueOfField('lowestTemperature')) + }, + { + name: 'someInteger', + type: 'number', + title: 'Some integer', + description: 'Only integers', + validation: Rule => Rule.integer() + }, + { + name: 'quotes', + title: 'Quotes', + description: 'Unique quotes - minimum of one', + validation: Rule => Rule.min(1).unique(), + type: 'array', + of: [ + { + type: 'string', + validation: Rule => Rule.min(2).max(100) + } + ] + }, + { + name: 'authors', + title: 'Authors', + description: 'Unique inline authors', + validation: Rule => Rule.unique(), + type: 'array', + of: [ + { + type: 'author' + } + ] + }, + { + name: 'books', + title: 'Books', + description: 'Unique book references, minimum 1, max 5', + validation: Rule => + Rule.unique() + .min(1) + .max(5), + type: 'array', + of: [ + { + type: 'reference', + to: [{type: 'book'}] + } + ] + }, + { + name: 'bookWithCover', + title: 'Book that has a cover photo', + description: 'Reference to a book with custom rule that ensures referenced book has a cover', + type: 'reference', + to: [{type: 'book'}], + validation: Rule => + Rule.custom( + value => + new Promise(resolve => { + if (!value || !value._ref) { + return resolve(true) + } + + return client.fetch('*[_id == $id][0].coverImage', {id: value._ref}).then(cover => { + resolve(cover ? true : 'Referenced book must have a cover image') + }) + }) + ) + }, + { + name: 'titleCase', + title: 'Title Case', + description: 'Regex-based title case, custom error message', + type: 'string', + validation: Rule => + Rule.min(1) + .regex(/^(?:[A-Z][^\s]*\s?)+$/) + .error('Must be in Title Case') + }, + { + name: 'translations', + title: 'Translations', + description: 'Needs at least one field to be valid', + type: 'object', + validation: Rule => Rule.required(), + fields: [ + {name: 'no', type: 'string', title: 'Norwegian (BokmÄl)'}, + {name: 'nn', type: 'string', title: 'Norwegian (Nynorsk)'}, + {name: 'se', type: 'string', title: 'Swedish'} + ] + }, + { + name: 'image', + title: 'Image', + type: 'image', + options: {hotspot: true} + }, + { + name: 'dropdown', + title: 'Dropdown thing', + description: 'Inferred to have one of the defined values, and explicitly set as required', + type: 'string', + validation: Rule => Rule.required(), + options: { + list: ['one', 'two', 'three'] + } + }, + { + name: 'radio', + title: 'Radio thing', + type: 'string', + description: 'Same as above, but as radio', + options: { + layout: 'radio', + list: ['one', 'two', 'three'] + } + }, + { + name: 'readonlyField', + type: 'string', + title: 'A read only string', + description: 'It may have a value, but it cannot be edited', + validation: Rule => Rule.required().min(5), + readOnly: true + }, + { + name: 'switch', + type: 'boolean', + title: 'Check me?', + validation: Rule => Rule.required().valid(true), + description: 'Must be true' + }, + { + name: 'checkbox', + type: 'boolean', + title: 'Checked?', + description: 'Must be false, should be displayed as a checkbox', + validation: Rule => Rule.required().valid(false), + options: { + layout: 'checkbox' + } + }, + { + name: 'person', + type: 'object', + fieldsets: [{name: 'social', title: 'Social media handles'}], + fields: [ + { + name: 'name', + title: 'Name', + type: 'string' + }, + { + name: 'twitter', + title: 'Twitter', + type: 'string', + description: 'Required within a fieldset', + validation: Rule => Rule.required(), + fieldset: 'social' + }, + { + name: 'instagram', + title: 'Instagram', + type: 'string', + fieldset: 'social' + }, + { + name: 'facebook', + title: 'Facebook', + type: 'string', + fieldset: 'social' + } + ] + }, + + { + name: 'arrayOfPredefined', + title: 'Array of predefined options', + description: 'We required at least one of these to be checked', + type: 'array', + validation: Rule => Rule.required().min(1), + of: [ + { + type: 'reference', + to: [{type: 'author'}] + } + ], + options: { + direction: 'vertical', + list: [ + {_type: 'reference', _key: 'espen', _ref: 'espen'}, + {_type: 'reference', _key: 'bjoerge', _ref: 'bjoerge'} + ] + } + }, + + { + name: 'location', + type: 'geopoint', + title: 'A geopoint', + description: 'Required, must be in Norway somewhere', + validation: Rule => + Rule.required().custom(geoPoint => { + if (!geoPoint) { + return true + } + + const location = points([[geoPoint.lng, geoPoint.lat]]) + const norwayFeature = featureCollection(norway) + const ptsWithin = pointsWithinPolygon(location, norwayFeature) + return ptsWithin.features.length > 0 ? true : 'Location must be in Norway' + }) + }, + + { + name: 'body', + title: 'Block content', + description: 'Requires', + type: 'array', + of: [ + {type: 'image', title: 'Image', options: {inline: true}}, + {type: 'author', title: 'Author'}, + { + type: 'block', + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + {title: 'Quote', value: 'blockquote'} + ], + lists: [{title: 'Bullet', value: 'bullet'}, {title: 'Numbered', value: 'number'}], + marks: { + decorators: [{title: 'Strong', value: 'strong'}, {title: 'Emphasis', value: 'em'}], + annotations: [ + {name: 'Author', title: 'Author', type: 'reference', to: {type: 'author'}} + ] + } + } + ] + }, + + { + name: 'arrayOfSlugs', + title: 'Array of slugs', + type: 'array', + of: [ + { + type: 'object', + name: 'slugEmbed', + fields: [ + { + type: 'slug', + name: 'sku', + title: 'SKU' + } + ] + } + ] + }, + + { + name: 'deepInline', + type: 'object', + title: 'Deep inline object', + description: 'Because why not, right?', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'deeper', + title: 'Child', + type: 'object', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'deeper', + title: 'Child', + type: 'object', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'deeper', + title: 'Child', + type: 'object', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string' + }, + { + name: 'deeper', + title: 'Child', + type: 'object', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string', + description: 'Required, uppercase', + validation: Rule => Rule.required().uppercase() + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/packages/backstop-test-studio/src/components/AuthorReferenceInput.css b/packages/backstop-test-studio/src/components/AuthorReferenceInput.css new file mode 100644 index 00000000000..e963384ce4d --- /dev/null +++ b/packages/backstop-test-studio/src/components/AuthorReferenceInput.css @@ -0,0 +1,20 @@ +.authorGroup { + max-width: 500px; +} + +.authorImage { + display: block; + width: 75px; + height: 75px; +} + +.button { + padding: 0; + margin: 2px; + border: 3px solid transparent; +} + +.activeButton { + composes: button; + border-color: lime; +} diff --git a/packages/backstop-test-studio/src/components/AuthorReferenceInput.js b/packages/backstop-test-studio/src/components/AuthorReferenceInput.js new file mode 100644 index 00000000000..13ef22d9f70 --- /dev/null +++ b/packages/backstop-test-studio/src/components/AuthorReferenceInput.js @@ -0,0 +1,137 @@ +import React from 'react' +import PropTypes from 'prop-types' +import imageUrlBuilder from '@sanity/image-url' +import client from 'part:@sanity/base/client' +import Spinner from 'part:@sanity/components/loading/spinner' +import FormField from 'part:@sanity/components/formfields/default' +import {PatchEvent, set, unset, setIfMissing} from 'part:@sanity/form-builder/patch-event' +import styles from './AuthorReferenceInput.css' + +const noop = () => null +const imageBuilder = imageUrlBuilder(client) + +export default class AuthorReferenceInput extends React.Component { + static propTypes = { + type: PropTypes.shape({ + title: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string + }).isRequired, + + value: PropTypes.shape({ + _ref: PropTypes.string.isRequired, + _type: PropTypes.string.isRequired + }), + + readOnly: PropTypes.bool, + level: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired + } + + static defaultProps = { + readOnly: false, + value: undefined + } + + state = {loading: true, authors: []} + + constructor() { + super() + + // In a real world scenario, apply some kind of constraint that is not + // based on whether or not the author has an image, obviously + this.fetchObservable = client.observable + .fetch( + // Select authors, with a defined image, which are published + '*[_type == "author" && defined(image) && _id in path("*")][0...10] {_id, image, name}' + ) + .subscribe(this.handleAuthorsReceived) + } + + componentWillUnmount() { + this.fetchObservable.unsubscribe() + } + + handleAuthorsReceived = authors => this.setState({authors, loading: false}) + + handleChange = item => { + const {type} = this.props + + // Are we selecting the same value as previously selected? + if (this.props.value && this.props.value._ref === item._id) { + // Clear the current value + this.handleClear() + return + } + + this.props.onChange( + PatchEvent.from( + // A reference is an object, so we need to initialize it before attempting to set subproperties + setIfMissing({ + _type: type.name, + _ref: item._id + }), + + // Allow setting weak reference in schema options + type.weak === true ? set(true, ['_weak']) : unset(['_weak']), + + // Set the actual reference value + set(item._id, ['_ref']) + ) + ) + } + + handleClear = () => { + this.props.onChange(PatchEvent.from(unset())) + } + + setFirstInputRef = input => { + this._input = input + } + + focus() { + if (this._input) { + this._input.focus() + } + } + + render() { + const {type, value, level, readOnly} = this.props + const {loading, authors} = this.state + + // What is the currently selected reference? + const current = value && value._ref + + return ( + +
+ {loading ? ( + + ) : ( + authors.map((author, i) => ( + + )) + )} +
+
+ ) + } +} diff --git a/packages/backstop-test-studio/src/components/JsonDocumentDump.js b/packages/backstop-test-studio/src/components/JsonDocumentDump.js new file mode 100644 index 00000000000..aea1e6f8f0d --- /dev/null +++ b/packages/backstop-test-studio/src/components/JsonDocumentDump.js @@ -0,0 +1,66 @@ +import React from 'react' +import PropTypes from 'prop-types' +import client from 'part:@sanity/base/client' +import Spinner from 'part:@sanity/components/loading/spinner' + +export default class JsonDocumentDump extends React.PureComponent { + static propTypes = { + itemId: PropTypes.string.isRequired + } + + state = {isLoading: true} + + actionHandlers = { + reload: () => this.setup() + } + + setup() { + this.dispose() + this.setState({isLoading: true, document: undefined}) + + const {itemId} = this.props + const draftId = `drafts.${itemId}` + const query = '*[_id in [$itemId, $draftId]]' + const params = {itemId, draftId} + + this.document$ = client.observable + .fetch(`${query} | order(_updatedAt desc) [0]`, params) + .subscribe(document => this.setState({document, isLoading: false})) + + this.documentListener$ = client.observable + .listen(query, params) + .subscribe(mut => this.setState({document: mut.result, isLoading: false})) + } + + dispose() { + if (this.document$) { + this.document$.unsubscribe() + this.documentListener$.unsubscribe() + } + } + + componentDidMount() { + this.setup() + } + + componentWillUnmount() { + this.dispose() + } + + render() { + const {isLoading, document} = this.state + if (isLoading) { + return + } + + if (!document) { + return
Document not found
+ } + + return ( +
+        {JSON.stringify(document, null, 2)}
+      
+ ) + } +} diff --git a/packages/backstop-test-studio/src/components/PertEstimate.css b/packages/backstop-test-studio/src/components/PertEstimate.css new file mode 100644 index 00000000000..9f01d80e5f4 --- /dev/null +++ b/packages/backstop-test-studio/src/components/PertEstimate.css @@ -0,0 +1,4 @@ +.pert-global { + font-family: monospace !important; + background: #bf1942; +} diff --git a/packages/backstop-test-studio/src/components/PertEstimate.js b/packages/backstop-test-studio/src/components/PertEstimate.js new file mode 100644 index 00000000000..aa15a189041 --- /dev/null +++ b/packages/backstop-test-studio/src/components/PertEstimate.js @@ -0,0 +1,2 @@ +export {default as Input} from './PertEstimateInput' +export {default as schema} from './PertEstimateSchema' diff --git a/packages/backstop-test-studio/src/components/PertEstimateInput.js b/packages/backstop-test-studio/src/components/PertEstimateInput.js new file mode 100644 index 00000000000..0ca5340f780 --- /dev/null +++ b/packages/backstop-test-studio/src/components/PertEstimateInput.js @@ -0,0 +1,68 @@ +import React from 'react' +import PropTypes from 'prop-types' +import {PatchEvent, set, unset, setIfMissing} from 'part:@sanity/form-builder/patch-event' +import './PertEstimate.css?raw' // eslint-disable-line + +const calculateEstimate = estimates => { + const {optimistic, nominal, pessimistic} = estimates || {} + if (!optimistic || !nominal || !pessimistic) { + return {optimistic, nominal, pessimistic, calculated: null} + } + + // ” = (O + (4 * N) + P) / 6 + const calculated = Number(((optimistic + 4 * nominal + pessimistic) / 6).toFixed(2)) + return {optimistic, nominal, pessimistic, calculated} +} + +export default class PertEstimateInput extends React.Component { + static propTypes = { + value: PropTypes.object, + type: PropTypes.object, + onChange: PropTypes.func + } + + handleChange = (field, event) => { + const {type, onChange} = this.props + const value = Object.assign({}, this.props.value || {}, { + [field.name]: event.target.valueAsNumber + }) + const {calculated} = calculateEstimate(value) + + onChange( + PatchEvent.from( + setIfMissing({_type: type.name}), + set(event.target.valueAsNumber, [field.name]), + calculated ? set(calculated, ['calculated']) : unset(['calculated']) + ) + ) + } + + render() { + const {value, type} = this.props + return ( +
+

{type.title}

+

{type.description}

+ + + {type.fields.filter(field => !field.type.readOnly).map(field => ( + + + + + ))} + +
{field.type.title} + this.handleChange(field, event)} + /> +
+ + {value && value.calculated &&

PERT-estimate: {value.calculated}

} +
+ ) + } +} diff --git a/packages/backstop-test-studio/src/components/PertEstimateSchema.js b/packages/backstop-test-studio/src/components/PertEstimateSchema.js new file mode 100644 index 00000000000..1f55e96fbc1 --- /dev/null +++ b/packages/backstop-test-studio/src/components/PertEstimateSchema.js @@ -0,0 +1,31 @@ +import PertEstimateInput from './PertEstimateInput' + +export default { + name: 'pertEstimate', + type: 'object', + title: 'PERT-estimate', + inputComponent: PertEstimateInput, + fields: [ + { + title: 'Optimistic estimate', + name: 'optimistic', + type: 'number' + }, + { + title: 'Nominal estimate', + name: 'nominal', + type: 'number' + }, + { + title: 'Pessimistic estimate', + name: 'pessimistic', + type: 'number' + }, + { + title: 'Pert estimate', + name: 'calculated', + type: 'number', + readOnly: true + } + ] +} diff --git a/packages/backstop-test-studio/src/components/SearchableArrayFunctions.css b/packages/backstop-test-studio/src/components/SearchableArrayFunctions.css new file mode 100644 index 00000000000..8a037ac3bb6 --- /dev/null +++ b/packages/backstop-test-studio/src/components/SearchableArrayFunctions.css @@ -0,0 +1,15 @@ +@import 'part:@sanity/base/theme/variables-style'; + +.item { + padding: var(--small-padding); +} + +.searchableSelect { + composes: spacing from 'part:@sanity/components/buttons/default-style'; + grid-column: span 2; + width: stretch; +} + +.searchableSelectInput { + width: 100%; +} diff --git a/packages/backstop-test-studio/src/components/SearchableArrayFunctions.js b/packages/backstop-test-studio/src/components/SearchableArrayFunctions.js new file mode 100644 index 00000000000..13783e6c427 --- /dev/null +++ b/packages/backstop-test-studio/src/components/SearchableArrayFunctions.js @@ -0,0 +1,83 @@ +import React from 'react' +import PropTypes from 'prop-types' +import SearchableSelect from 'part:@sanity/components/selects/searchable' +import DefaultArrayFunctions from 'part:@sanity/form-builder/input/array/functions-default' +import styles from './SearchableArrayFunctions.css' + +const SearchItem = item =>
{item.title}
+ +// eslint-disable-next-line react/no-multi-comp +export default class SearchableArrayFunctions extends React.Component { + static propTypes = { + type: PropTypes.shape({ + of: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string, + type: PropTypes.shape({ + name: PropTypes.string.isRequired + }).isRequired + }) + ) + }).isRequired, + value: PropTypes.array, + readOnly: PropTypes.bool, + onAppendItem: PropTypes.func.isRequired, + onCreateValue: PropTypes.func.isRequired + } + + static defaultProps = { + value: [], + readOnly: false + } + + constructor(props) { + super(props) + + this.state = {results: this.getMatchingResults('', props.type)} + } + + handleInsertItem = type => { + const {onCreateValue, onAppendItem} = this.props + const item = onCreateValue(type) + onAppendItem(item) + } + + getMatchingResults = (query, type) => { + return type.of + .filter(memberDef => (memberDef.title || memberDef.type.name).toLowerCase().includes(query)) + .map(memberDef => ({ + title: memberDef.title || memberDef.type.name, + type: memberDef.type + })) + } + + handleSearch = query => { + this.setState({results: this.getMatchingResults(query, this.props.type)}) + } + + handleChange = value => { + this.handleInsertItem(value.type) + } + + render() { + const {type, readOnly} = this.props + if (readOnly || type.of.length <= 3) { + return + } + + return ( + +
+ +
+
+ ) + } +} diff --git a/packages/backstop-test-studio/src/deskStructure.js b/packages/backstop-test-studio/src/deskStructure.js new file mode 100644 index 00000000000..0a3b7903b3b --- /dev/null +++ b/packages/backstop-test-studio/src/deskStructure.js @@ -0,0 +1,53 @@ +import React from 'react' +import S from '@sanity/desk-tool/structure-builder' + +export default () => + S.list() + .id('root') + .title('Content') + .items([ + ...S.documentTypeListItems(), + S.listItem() + .title('Deep panes') + .child( + S.list() + .title('Depth 1') + .items([ + S.listItem() + .title('Deeper') + .child( + S.list() + .title('Depth 2') + .items([ + S.listItem() + .title('Even deeper') + .child( + S.list() + .title('Depth 3') + .items([ + S.listItem() + .title('Keep digging') + .child( + S.list() + .title('Depth 4') + .items([ + S.listItem() + .title('Dig into the core of the earth') + .child( + S.list() + .title('Depth 5') + .items([ + S.documentListItem() + .id('grrm') + .schemaType('author') + ]) + ) + ]) + ) + ]) + ) + ]) + ) + ]) + ) + ]) diff --git a/packages/backstop-test-studio/src/resolveProductionUrl.js b/packages/backstop-test-studio/src/resolveProductionUrl.js new file mode 100644 index 00000000000..b617279507e --- /dev/null +++ b/packages/backstop-test-studio/src/resolveProductionUrl.js @@ -0,0 +1,5 @@ +const PREVIEW_TYPES = ['book', 'author'] + +export default function resolveProductionUrl(document) { + return PREVIEW_TYPES.includes(document._type) && `https://example.com/preview/${document._id}` +} diff --git a/packages/backstop-test-studio/src/test-intent-tool.js b/packages/backstop-test-studio/src/test-intent-tool.js new file mode 100644 index 00000000000..b521b6c88fa --- /dev/null +++ b/packages/backstop-test-studio/src/test-intent-tool.js @@ -0,0 +1,25 @@ +import React from 'react' +import {route, withRouterHOC} from 'part:@sanity/base/router' + +export default { + router: route('/:type/:id'), + canHandleIntent(intentName, params) { + return (intentName === 'edit' && params.id) || (intentName === 'create' && params.type) + }, + getIntentState(intentName, params) { + return { + type: params.type || '*', + id: params.id + } + }, + title: 'Test intent', + name: 'test-intent', + component: withRouterHOC(props => ( +
+

Test intent precedence

+ If you click an intent link (e.g. from search results) while this tool is open, it should be + opened here. +
{JSON.stringify(props.router.state, null, 2)}
+
+ )) +} diff --git a/packages/backstop-test-studio/static/.gitkeep b/packages/backstop-test-studio/static/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/backstop/.gitignore b/test/backstop/.gitignore new file mode 100644 index 00000000000..b38ef76ae81 --- /dev/null +++ b/test/backstop/.gitignore @@ -0,0 +1,4 @@ +# Backstop +bitmapsTest/ +htmlReport/ +ciReport/ diff --git a/test/backstop/backstop.js b/test/backstop/backstop.js new file mode 100644 index 00000000000..37cb9a2b8ef --- /dev/null +++ b/test/backstop/backstop.js @@ -0,0 +1,93 @@ +/* eslint-disable import/no-commonjs, camelcase, no-process-env */ +const URL = 'http://host.docker.internal:5000' + +const loggedInUserCookiePath = './test/backstop/engineScripts/cookies.json' + +module.exports = { + id: 'content_studio', + viewports: [ + { + label: 'desktop', + width: 1200, + height: 960 + }, + { + label: 'tablet-landscape', + width: 1024, + height: 768 + }, + { + label: 'tablet-portrait', + width: 768, + height: 1024 + }, + { + label: 'mobile', + width: 320, + height: 650 + } + ], + onBeforeScript: 'puppet/onBefore.js', + onReadyScript: 'puppet/onReady.js', + scenarios: [ + { + label: 'Login', + url: URL, + delay: 1000, + readySelector: '[class^="LoginDialogContent_root"]', + selectors: ['viewport'], + selectorExpansion: true, + misMatchThreshold: 0.1, + requireSameDimensions: true + }, + { + label: 'Front', + cookiePath: loggedInUserCookiePath, + url: URL, + delay: 1000, + readySelector: '[class^="PaneItem_item"]', + selectors: ['viewport'], + selectorExpansion: true, + misMatchThreshold: 0.1, + requireSameDimensions: true + }, + { + label: 'Book pane', + cookiePath: loggedInUserCookiePath, + url: `${URL}/desk/book`, + delay: 1000, + readySelector: '[class^="PaneItem_item"]', + selectors: ['viewport'], + selectorExpansion: true, + misMatchThreshold: 0.1, + requireSameDimensions: true + }, + { + label: 'Book document', + cookiePath: loggedInUserCookiePath, + url: `${URL}/desk/book;c6b1208f-cd89-4a55-88f1-0b979e005f0a`, + delay: 1000, + readySelector: '[class^="Editor_root"]', + selectors: ['viewport'], + selectorExpansion: true, + misMatchThreshold: 0.1, + requireSameDimensions: true + } + ], + paths: { + bitmaps_reference: 'test/backstop/bitmapsReference', + bitmaps_test: 'test/backstop/bitmapsTest', + engine_scripts: 'test/backstop/engineScripts', + html_report: 'test/backstop/htmlReport', + ci_report: 'test/backstop/ciReport' + }, + report: process.env.CI ? ['browser', 'CI'] : ['browser'], + engine: 'puppeteer', + engineOptions: { + args: ['--no-sandbox'] + }, + asyncCaptureLimit: 3, + asyncCompareLimit: 50, + debug: false, + debugWindow: false +} diff --git a/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_0_desktop.png b/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_0_desktop.png new file mode 100644 index 00000000000..279edc46ff3 Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_0_desktop.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_1_tablet-landscape.png b/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_1_tablet-landscape.png new file mode 100644 index 00000000000..b246402d3a8 Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_1_tablet-landscape.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_2_tablet-portrait.png b/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_2_tablet-portrait.png new file mode 100644 index 00000000000..8be97a776c1 Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_2_tablet-portrait.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_3_mobile.png b/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_3_mobile.png new file mode 100644 index 00000000000..c822fc809bb Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Book_document_0_viewport_3_mobile.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_0_desktop.png b/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_0_desktop.png new file mode 100644 index 00000000000..ff04b53daae Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_0_desktop.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_1_tablet-landscape.png b/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_1_tablet-landscape.png new file mode 100644 index 00000000000..d0dd304ec7f Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_1_tablet-landscape.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_2_tablet-portrait.png b/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_2_tablet-portrait.png new file mode 100644 index 00000000000..43f348b1771 Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_2_tablet-portrait.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_3_mobile.png b/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_3_mobile.png new file mode 100644 index 00000000000..a81f668c7ad Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Book_pane_0_viewport_3_mobile.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Front_0_viewport_0_desktop.png b/test/backstop/bitmapsReference/content_studio_Front_0_viewport_0_desktop.png new file mode 100644 index 00000000000..d629733ff8c Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Front_0_viewport_0_desktop.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Front_0_viewport_1_tablet-landscape.png b/test/backstop/bitmapsReference/content_studio_Front_0_viewport_1_tablet-landscape.png new file mode 100644 index 00000000000..92ff6eb117d Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Front_0_viewport_1_tablet-landscape.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Front_0_viewport_2_tablet-portrait.png b/test/backstop/bitmapsReference/content_studio_Front_0_viewport_2_tablet-portrait.png new file mode 100644 index 00000000000..1c909ca5ebe Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Front_0_viewport_2_tablet-portrait.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Front_0_viewport_3_mobile.png b/test/backstop/bitmapsReference/content_studio_Front_0_viewport_3_mobile.png new file mode 100644 index 00000000000..c83be12bc07 Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Front_0_viewport_3_mobile.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Login_0_viewport_0_desktop.png b/test/backstop/bitmapsReference/content_studio_Login_0_viewport_0_desktop.png new file mode 100644 index 00000000000..44f2cb7e200 Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Login_0_viewport_0_desktop.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Login_0_viewport_1_tablet-landscape.png b/test/backstop/bitmapsReference/content_studio_Login_0_viewport_1_tablet-landscape.png new file mode 100644 index 00000000000..cc8af6689bf Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Login_0_viewport_1_tablet-landscape.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Login_0_viewport_2_tablet-portrait.png b/test/backstop/bitmapsReference/content_studio_Login_0_viewport_2_tablet-portrait.png new file mode 100644 index 00000000000..8acd21abb19 Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Login_0_viewport_2_tablet-portrait.png differ diff --git a/test/backstop/bitmapsReference/content_studio_Login_0_viewport_3_mobile.png b/test/backstop/bitmapsReference/content_studio_Login_0_viewport_3_mobile.png new file mode 100644 index 00000000000..aaecd309c66 Binary files /dev/null and b/test/backstop/bitmapsReference/content_studio_Login_0_viewport_3_mobile.png differ diff --git a/test/backstop/engineScripts/cookies.json b/test/backstop/engineScripts/cookies.json new file mode 100644 index 00000000000..7742e65c3c6 --- /dev/null +++ b/test/backstop/engineScripts/cookies.json @@ -0,0 +1,14 @@ +[ + { + "domain": ".xtlrc4xo.api.sanity.io", + "path": "/v1/users/me", + "name": "sanitySession", + "value": "sktOMwifahxA73ddSCMKRGcaS75nvh4kZTudOjjnOlze7SmgBdeeilP73JEJcRIZUD8GAhJeENqKXcQSx3J4PxJL0btsuzQmHrgKVeeIBROj7KsLkckxZMVkpom6XsMM1j2DYyq60z4f9V3kPTJG1uOxHPFmx5QskEsQIGfruPMprm1yFLgI", + "expirationDate": 2143588996000, + "hostOnly": true, + "httpOnly": true, + "secure": false, + "session": true, + "sameSite": "no_restriction" + } +] diff --git a/test/backstop/engineScripts/puppet/clickAndHoverHelper.js b/test/backstop/engineScripts/puppet/clickAndHoverHelper.js new file mode 100644 index 00000000000..ffb1a867515 --- /dev/null +++ b/test/backstop/engineScripts/puppet/clickAndHoverHelper.js @@ -0,0 +1,32 @@ +/* eslint-disable import/no-commonjs, no-var, no-await-in-loop, no-shadow */ +module.exports = async (page, scenario) => { + var hoverSelector = scenario.hoverSelectors || scenario.hoverSelector + var clickSelector = scenario.clickSelectors || scenario.clickSelector + var scrollToSelector = scenario.scrollToSelector + var postInteractionWait = scenario.postInteractionWait + + if (hoverSelector) { + for (const hoverSelectorIndex of [].concat(hoverSelector)) { + await page.waitFor(hoverSelectorIndex) + await page.hover(hoverSelectorIndex) + } + } + + if (clickSelector) { + for (const clickSelectorIndex of [].concat(clickSelector)) { + await page.waitFor(clickSelectorIndex) + await page.click(clickSelectorIndex) + } + } + + if (postInteractionWait) { + await page.waitFor(postInteractionWait) + } + + if (scrollToSelector) { + await page.waitFor(scrollToSelector) + await page.evaluate(scrollToSelector => { + document.querySelector(scrollToSelector).scrollIntoView() + }, scrollToSelector) + } +} diff --git a/test/backstop/engineScripts/puppet/loadCookies.js b/test/backstop/engineScripts/puppet/loadCookies.js new file mode 100644 index 00000000000..dbc711125f3 --- /dev/null +++ b/test/backstop/engineScripts/puppet/loadCookies.js @@ -0,0 +1,29 @@ +/* eslint-disable import/no-commonjs, no-var, no-await-in-loop, no-shadow */ +var fs = require('fs') + +module.exports = async (page, scenario) => { + var cookies = [] + var cookiePath = scenario.cookiePath + + // READ COOKIES FROM FILE IF EXISTS + if (fs.existsSync(cookiePath)) { + cookies = JSON.parse(fs.readFileSync(cookiePath)) + } + + // MUNGE COOKIE DOMAIN + cookies = cookies.map(cookie => { + cookie.url = `https://${cookie.domain}` + delete cookie.domain + return cookie + }) + + // SET COOKIES + const setCookies = async () => { + return Promise.all( + cookies.map(async cookie => { + await page.setCookie(cookie) + }) + ) + } + await setCookies() +} diff --git a/test/backstop/engineScripts/puppet/onBefore.js b/test/backstop/engineScripts/puppet/onBefore.js new file mode 100644 index 00000000000..07797ed4a84 --- /dev/null +++ b/test/backstop/engineScripts/puppet/onBefore.js @@ -0,0 +1,13 @@ +/* eslint-disable import/no-commonjs, no-console */ +;(function(original) { + console.enableLogging = () => { + console.log = original + } + console.disableLogging = () => { + console.log = () => {} + } +})(console.log) + +module.exports = async (page, scenario, vp) => { + await require('./loadCookies')(page, scenario) +} diff --git a/test/backstop/engineScripts/puppet/onReady.js b/test/backstop/engineScripts/puppet/onReady.js new file mode 100644 index 00000000000..c5aa61775a6 --- /dev/null +++ b/test/backstop/engineScripts/puppet/onReady.js @@ -0,0 +1,8 @@ +/* eslint-disable import/no-commonjs, no-console */ +module.exports = async (page, scenario, vp) => { + console.enableLogging() + console.log(`SCENARIO > ${scenario.label} (${vp.label}) ${scenario.url}`) + await require('./clickAndHoverHelper')(page, scenario) + console.disableLogging() + // add more ready handlers here... +}