feat: bundle assets with content (#2958)

* fix(media_folder_relative): use collection name in unpublished entry

* refactor: pass arguments as object to AssetProxy ctor

* feat: support media folders per collection

* feat: resolve media files path based on entry path

* fix: asset public path resolving

* refactor: introduce typescript for AssetProxy

* refactor: code cleanup

* refactor(asset-proxy): add tests,switch to typescript,extract arguments

* refactor: typescript for editorialWorkflow

* refactor: add typescript for media library actions

* refactor: fix type error on map set

* refactor: move locale selector into reducer

* refactor: add typescript for entries actions

* refactor: remove duplication between asset store and media lib

* feat: load assets from backend using API

* refactor(github): add typescript, cache media files

* fix: don't load media URL if already loaded

* feat: add media folder config to collection

* fix: load assets from API when not in UI state

* feat: load entry media files when opening media library

* fix: editorial workflow draft media files bug fixes

* test(unit): fix unit tests

* fix: editor control losing focus

* style: add eslint object-shorthand rule

* test(cypress): re-record mock data

* fix: fix non github backends, large media

* test: uncomment only in tests

* fix(backend-test): add missing displayURL property

* test(e2e): add media library tests

* test(e2e): enable visual testing

* test(e2e): add github backend media library tests

* test(e2e): add git-gateway large media tests

* chore: post rebase fixes

* test: fix tests

* test: fix tests

* test(cypress): fix tests

* docs: add media_folder docs

* test(e2e): add media library delete test

* test(e2e): try and fix image comparison on CI

* ci: reduce test machines from 9 to 8

* test: add reducers and selectors unit tests

* test(e2e): disable visual regression testing for now

* test: add getAsset unit tests

* refactor: use Asset class component instead of hooks

* build: don't inline source maps

* test: add more media path tests
This commit is contained in:
Erez Rokah
2019-12-18 18:16:02 +02:00
committed by Shawn Erquhart
parent 7e4d4c1cc4
commit 2b41d8a838
231 changed files with 37961 additions and 18373 deletions

View File

@ -57,26 +57,39 @@ const stubFetch = (win, routes) => {
const route = routes.splice(routeIndex, 1)[0];
console.log(`matched ${args[0]} to ${route.url} ${route.method} ${route.status}`);
const response = {
let blob;
if (route.response && route.response.encoding === 'base64') {
const buffer = Buffer.from(route.response.content, 'base64');
blob = new Blob([buffer]);
} else {
blob = new Blob([route.response || '']);
}
const fetchResponse = {
status: route.status,
headers: new Headers(route.headers),
blob: () => Promise.resolve(blob),
text: () => Promise.resolve(route.response),
json: () => Promise.resolve(JSON.parse(route.response)),
ok: route.status >= 200 && route.status <= 299,
};
return Promise.resolve(response);
} else if (args[0].includes('api.github.com')) {
return Promise.resolve(fetchResponse);
} else if (
args[0].includes('api.github.com') ||
args[0].includes('netlify.com') ||
args[0].includes('s3.amazonaws.com')
) {
console.warn(
`No route match for github api request. Fetch args: ${JSON.stringify(args)}. Returning 404`,
`No route match for api request. Fetch args: ${JSON.stringify(args)}. Returning 404`,
);
const response = {
const fetchResponse = {
status: 404,
headers: new Headers(),
blob: () => Promise.resolve(new Blob(['{}'])),
text: () => Promise.resolve('{}'),
json: () => Promise.resolve({}),
ok: false,
};
return Promise.resolve(response);
return Promise.resolve(fetchResponse);
} else {
console.log(`No route match for fetch args: ${JSON.stringify(args)}`);
return fetch(...args);
@ -91,7 +104,8 @@ Cypress.Commands.add('stubFetch', ({ fixture }) => {
});
function runTimes(cyInstance, fn, count = 1) {
let chain = cyInstance, i = count;
let chain = cyInstance,
i = count;
while (i) {
i -= 1;
chain = fn(chain);
@ -108,7 +122,7 @@ function runTimes(cyInstance, fn, count = 1) {
['left', 'leftArrow'],
['right', 'rightArrow'],
].forEach(key => {
const [ cmd, keyName ] = typeof key === 'object' ? key : [key, key];
const [cmd, keyName] = typeof key === 'object' ? key : [key, key];
Cypress.Commands.add(cmd, { prevSubject: true }, (subject, { shift, times = 1 } = {}) => {
const fn = chain => chain.type(`${shift ? '{shift}' : ''}{${keyName}}`);
return runTimes(cy.wrap(subject), fn, times);
@ -125,7 +139,7 @@ Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => {
cy.wrap(subject)
.trigger('mousedown')
.then(fn)
.trigger('mouseup')
.trigger('mouseup');
cy.document().trigger('selectionchange');
return cy.wrap(subject);
@ -138,36 +152,36 @@ Cypress.Commands.add('print', { prevSubject: 'optional' }, (subject, str) => {
});
Cypress.Commands.add('setSelection', { prevSubject: true }, (subject, query, endQuery) => {
return cy.wrap(subject)
.selection($el => {
if (typeof query === 'string') {
const anchorNode = getTextNode($el[0], query);
const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode;
const anchorOffset = anchorNode.wholeText.indexOf(query);
const focusOffset = endQuery ?
focusNode.wholeText.indexOf(endQuery) + endQuery.length :
anchorOffset + query.length;
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
} else if (typeof query === 'object') {
const el = $el[0];
const anchorNode = getTextNode(el.querySelector(query.anchorQuery));
const anchorOffset = query.anchorOffset || 0;
const focusNode = query.focusQuery ? getTextNode(el.querySelector(query.focusQuery)) : anchorNode;
const focusOffset = query.focusOffset || 0;
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
}
});
return cy.wrap(subject).selection($el => {
if (typeof query === 'string') {
const anchorNode = getTextNode($el[0], query);
const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode;
const anchorOffset = anchorNode.wholeText.indexOf(query);
const focusOffset = endQuery
? focusNode.wholeText.indexOf(endQuery) + endQuery.length
: anchorOffset + query.length;
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
} else if (typeof query === 'object') {
const el = $el[0];
const anchorNode = getTextNode(el.querySelector(query.anchorQuery));
const anchorOffset = query.anchorOffset || 0;
const focusNode = query.focusQuery
? getTextNode(el.querySelector(query.focusQuery))
: anchorNode;
const focusOffset = query.focusOffset || 0;
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
}
});
});
Cypress.Commands.add('setCursor', { prevSubject: true }, (subject, query, atStart) => {
return cy.wrap(subject)
.selection($el => {
const node = getTextNode($el[0], query);
const offset = node.wholeText.indexOf(query) + (atStart ? 0 : query.length);
const document = node.ownerDocument;
document.getSelection().removeAllRanges();
document.getSelection().collapse(node, offset);
});
return cy.wrap(subject).selection($el => {
const node = getTextNode($el[0], query);
const offset = node.wholeText.indexOf(query) + (atStart ? 0 : query.length);
const document = node.ownerDocument;
document.getSelection().removeAllRanges();
document.getSelection().collapse(node, offset);
});
});
Cypress.Commands.add('setCursorBefore', { prevSubject: true }, (subject, query) => {
@ -190,23 +204,21 @@ Cypress.Commands.add('loginAndNewPost', () => {
});
Cypress.Commands.add('drag', { prevSubject: true }, subject => {
return cy.wrap(subject)
.trigger('dragstart', {
dataTransfer: {},
force: true,
});
return cy.wrap(subject).trigger('dragstart', {
dataTransfer: {},
force: true,
});
});
Cypress.Commands.add('drop', { prevSubject: true }, subject => {
return cy.wrap(subject)
.trigger('drop', {
dataTransfer: {},
force: true,
});
return cy.wrap(subject).trigger('drop', {
dataTransfer: {},
force: true,
});
});
Cypress.Commands.add('clickToolbarButton', (title, { times } = {}) => {
const isHeading = title.startsWith('Heading')
const isHeading = title.startsWith('Heading');
if (isHeading) {
cy.get('button[title="Headings"]').click();
}
@ -216,11 +228,12 @@ Cypress.Commands.add('clickToolbarButton', (title, { times } = {}) => {
});
Cypress.Commands.add('insertEditorComponent', title => {
cy.get('button[title="Add Component"]').click()
cy.contains('div', title).click().focused();
cy.get('button[title="Add Component"]').click();
cy.contains('div', title)
.click()
.focused();
});
[
['clickHeadingOneButton', 'Heading 1'],
['clickHeadingTwoButton', 'Heading 2'],
@ -241,36 +254,33 @@ Cypress.Commands.add('clickModeToggle', () => {
.focused();
});
[
['insertCodeBlock', 'Code Block'],
].forEach(([commandName, componentTitle]) => {
[['insertCodeBlock', 'Code Block']].forEach(([commandName, componentTitle]) => {
Cypress.Commands.add(commandName, () => {
return cy.insertEditorComponent(componentTitle);
});
});
Cypress.Commands.add('getMarkdownEditor', () => {
return cy.get('[data-slate-editor]');
});
Cypress.Commands.add('confirmMarkdownEditorContent', expectedDomString => {
return cy.getMarkdownEditor()
.should(([element]) => {
// Slate makes the following representations:
// - blank line: 2 BOM's + <br>
// - blank element (placed inside empty elements): 1 BOM + <br>
// We replace to represent a blank line as a single <br>, and remove the
// contents of elements that are actually empty.
const actualDomString = toPlainTree(element.innerHTML)
.replace(/\uFEFF\uFEFF<br>/g, '<br>')
.replace(/\uFEFF<br>/g, '');
expect(actualDomString).toEqual(oneLineTrim(expectedDomString));
});
return cy.getMarkdownEditor().should(([element]) => {
// Slate makes the following representations:
// - blank line: 2 BOM's + <br>
// - blank element (placed inside empty elements): 1 BOM + <br>
// We replace to represent a blank line as a single <br>, and remove the
// contents of elements that are actually empty.
const actualDomString = toPlainTree(element.innerHTML)
.replace(/\uFEFF\uFEFF<br>/g, '<br>')
.replace(/\uFEFF<br>/g, '');
expect(actualDomString).toEqual(oneLineTrim(expectedDomString));
});
});
Cypress.Commands.add('clearMarkdownEditorContent', () => {
return cy.getMarkdownEditor()
return cy
.getMarkdownEditor()
.selectAll()
.backspace({ times: 2 });
});
@ -279,8 +289,7 @@ function toPlainTree(domString) {
return rehype()
.use(removeSlateArtifacts)
.data('settings', { fragment: true })
.processSync(domString)
.contents;
.processSync(domString).contents;
}
function getActualBlockChildren(node) {
@ -304,18 +313,17 @@ function removeSlateArtifacts() {
node.children = node.children.flatMap(getActualBlockChildren);
}
});
}
};
}
function getTextNode(el, match){
function getTextNode(el, match) {
const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
if (!match) {
return walk.nextNode();
}
const nodes = [];
let node;
while(node = walk.nextNode()) {
while ((node = walk.nextNode())) {
if (node.wholeText.includes(match)) {
return node;
}

View File

@ -12,11 +12,16 @@
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
require('cypress-plugin-tab');
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
import 'cypress-plugin-tab';
import 'cypress-file-upload';
import 'cypress-jest-adapter';
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';
addMatchImageSnapshotCommand({
failureThreshold: 0.01,
failureThresholdType: 'percent',
customDiffConfig: { threshold: 0.05 },
capture: 'viewport',
});
import './commands';