Feat: multi content authoring (#4139)

This commit is contained in:
Erez Rokah
2020-09-20 10:30:46 -07:00
committed by GitHub
parent 7968e01e29
commit cb2ad687ee
65 changed files with 4331 additions and 1521 deletions

View File

@ -0,0 +1,59 @@
import { newPost, populateEntry, publishEntry, flushClockAndSave } from '../../utils/steps';
const enterTranslation = str => {
cy.get(`[id^="title-field"]`)
.first()
.clear({ force: true });
cy.get(`[id^="title-field"]`)
.first()
.type(str, { force: true });
};
const createAndTranslate = entry => {
newPost();
// fill the main entry
populateEntry(entry, () => undefined);
// fill the translation
cy.get('.Pane2').within(() => {
enterTranslation('de');
cy.contains('span', 'Writing in DE').click();
cy.contains('span', 'fr').click();
enterTranslation('fr');
});
};
export const updateTranslation = () => {
cy.get('.Pane2').within(() => {
enterTranslation('fr fr');
cy.contains('span', 'Writing in FR').click();
cy.contains('span', 'de').click();
enterTranslation('de de');
});
flushClockAndSave();
};
export const assertTranslation = () => {
cy.get('.Pane2').within(() => {
cy.get(`[id^="title-field"]`).should('have.value', 'de');
cy.contains('span', 'Writing in DE').click();
cy.contains('span', 'fr').click();
cy.get(`[id^="title-field"]`).should('have.value', 'fr');
});
};
export const createEntryTranslateAndPublish = entry => {
createAndTranslate(entry);
publishEntry();
};
export const createEntryTranslateAndSave = entry => {
createAndTranslate(entry);
flushClockAndSave();
};

View File

@ -0,0 +1,54 @@
import '../../utils/dismiss-local-backup';
import {
login,
goToWorkflow,
updateWorkflowStatus,
exitEditor,
publishWorkflowEntry,
goToEntry,
updateWorkflowStatusInEditor,
publishEntryInEditor,
assertPublishedEntryInEditor,
assertUnpublishedEntryInEditor,
assertUnpublishedChangesInEditor,
} from '../../utils/steps';
import { createEntryTranslateAndSave, assertTranslation, updateTranslation } from './i18n';
import { workflowStatus, editorStatus, publishTypes } from '../../utils/constants';
export default function({ entry, getUser }) {
const structures = ['multiple_folders', 'multiple_files', 'single_file'];
structures.forEach(structure => {
it(`can create and publish entry with translation in ${structure} mode`, () => {
cy.task('updateConfig', { i18n: { structure } });
login(getUser());
createEntryTranslateAndSave(entry);
assertUnpublishedEntryInEditor();
exitEditor();
goToWorkflow();
updateWorkflowStatus(entry, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry);
goToEntry(entry);
assertTranslation();
assertPublishedEntryInEditor();
});
it(`can update translated entry in ${structure} mode`, () => {
cy.task('updateConfig', { i18n: { structure: 'multiple_folders' } });
login(getUser());
createEntryTranslateAndSave(entry);
assertUnpublishedEntryInEditor();
updateWorkflowStatusInEditor(editorStatus.ready);
publishEntryInEditor(publishTypes.publishNow);
exitEditor();
goToEntry(entry);
assertTranslation();
assertPublishedEntryInEditor();
updateTranslation();
assertUnpublishedChangesInEditor();
});
});
}

View File

@ -0,0 +1,38 @@
import fixture from './common/i18n_editorial_workflow_spec';
const backend = 'test';
describe(`I18N Test Backend Editorial Workflow`, () => {
const taskResult = { data: {} };
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', {
backend,
options: {
publish_mode: 'editorial_workflow',
i18n: {
locales: ['en', 'de', 'fr'],
},
collections: [
{
folder: 'content/i18n',
i18n: true,
fields: [{ i18n: true }, {}, { i18n: 'duplicate' }],
},
],
},
});
});
after(() => {
cy.task('teardownBackend', { backend });
});
const entry = {
title: 'first title',
body: 'first body',
};
fixture({ entry, getUser: () => taskResult.data.user });
});

View File

@ -0,0 +1,148 @@
import * as specUtils from './common/spec_utils';
import { login } from '../utils/steps';
import { createEntryTranslateAndPublish } from './common/i18n';
const backend = 'proxy';
const mode = 'fs';
const expectedEnContent = `---
template: post
title: first title
date: 1970-01-01T00:00:00.000Z
description: first description
category: first category
tags:
- tag1
---
`;
const expectedDeContent = `---
title: de
date: 1970-01-01T00:00:00.000Z
---
`;
const expectedFrContent = `---
title: fr
date: 1970-01-01T00:00:00.000Z
---
`;
const contentSingleFile = `---
en:
template: post
date: 1970-01-01T00:00:00.000Z
title: first title
description: first description
category: first category
tags:
- tag1
body: first body
de:
date: 1970-01-01T00:00:00.000Z
title: de
fr:
date: 1970-01-01T00:00:00.000Z
title: fr
---
`;
describe(`I18N Proxy Backend Simple Workflow - '${mode}' mode`, () => {
const taskResult = { data: {} };
const entry = {
title: 'first title',
body: 'first body',
description: 'first description',
category: 'first category',
tags: 'tag1',
};
before(() => {
specUtils.before(
taskResult,
{
mode,
publish_mode: 'simple',
i18n: {
locales: ['en', 'de', 'fr'],
},
collections: [{ i18n: true, fields: [{}, { i18n: true }, {}, { i18n: 'duplicate' }] }],
},
backend,
);
Cypress.config('taskTimeout', 15 * 1000);
Cypress.config('defaultCommandTimeout', 5 * 1000);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
it('can create entry with translation in locale_folders mode', () => {
cy.task('updateConfig', { i18n: { structure: 'multiple_folders' } });
login(taskResult.data.user);
createEntryTranslateAndPublish(entry);
cy.readFile(`${taskResult.data.tempDir}/content/posts/en/1970-01-01-first-title.md`).should(
'contain',
expectedEnContent,
);
cy.readFile(`${taskResult.data.tempDir}/content/posts/de/1970-01-01-first-title.md`).should(
'eq',
expectedDeContent,
);
cy.readFile(`${taskResult.data.tempDir}/content/posts/fr/1970-01-01-first-title.md`).should(
'eq',
expectedFrContent,
);
});
it('can create entry with translation in single_file mode', () => {
cy.task('updateConfig', { i18n: { structure: 'multiple_files' } });
login(taskResult.data.user);
createEntryTranslateAndPublish(entry);
cy.readFile(`${taskResult.data.tempDir}/content/posts/1970-01-01-first-title.en.md`).should(
'contain',
expectedEnContent,
);
cy.readFile(`${taskResult.data.tempDir}/content/posts/1970-01-01-first-title.de.md`).should(
'eq',
expectedDeContent,
);
cy.readFile(`${taskResult.data.tempDir}/content/posts/1970-01-01-first-title.fr.md`).should(
'eq',
expectedFrContent,
);
});
it('can create entry with translation in locale_file_extensions mode', () => {
cy.task('updateConfig', { i18n: { structure: 'single_file' } });
login(taskResult.data.user);
createEntryTranslateAndPublish(entry);
cy.readFile(`${taskResult.data.tempDir}/content/posts/1970-01-01-first-title.md`).should(
'eq',
contentSingleFile,
);
});
});

View File

@ -67,7 +67,7 @@ function codeBlock(content) {
<div>
<div></div>
<div>
<div><label>Code Block</label>
<div><label>Code Block </label>
<div><button><span><svg>
<path></path>
</svg></span></button>

View File

@ -6,7 +6,7 @@ const backend = 'proxy';
const mode = 'fs';
describe(`Proxy Backend Simple Workflow - '${mode}' mode`, () => {
let taskResult = { data: {} };
const taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'simple', mode }, backend);

View File

@ -302,14 +302,11 @@ async function teardownGitGatewayTest(taskData) {
transformRecordedData: (expectation, toSanitize) => {
const result = methods[taskData.provider].transformData(expectation, toSanitize);
const { httpRequest, httpResponse } = expectation;
if (httpResponse.body && httpRequest.path === '/.netlify/identity/token') {
const parsed = JSON.parse(httpResponse.body);
if (result.response && result.url === '/.netlify/identity/token') {
const parsed = JSON.parse(result.response);
parsed.access_token = 'access_token';
parsed.refresh_token = 'refresh_token';
const responseBody = JSON.stringify(parsed);
return { ...result, response: responseBody };
return { ...result, response: JSON.stringify(parsed) };
} else {
return result;
}

View File

@ -310,7 +310,11 @@ const transformRecordedData = (expectation, toSanitize) => {
const requestBodySanitizer = httpRequest => {
let body;
if (httpRequest.body && httpRequest.body.type === 'JSON' && httpRequest.body.json) {
const bodyObject = JSON.parse(httpRequest.body.json);
const bodyObject =
typeof httpRequest.body.json === 'string'
? JSON.parse(httpRequest.body.json)
: httpRequest.body.json;
if (bodyObject.encoding === 'base64') {
// sanitize encoded data
const decodedBody = Buffer.from(bodyObject.content, 'base64').toString('binary');
@ -319,10 +323,14 @@ const transformRecordedData = (expectation, toSanitize) => {
bodyObject.content = sanitizedEncodedContent;
body = JSON.stringify(bodyObject);
} else {
body = httpRequest.body.json;
body = JSON.stringify(bodyObject);
}
} else if (httpRequest.body && httpRequest.body.type === 'STRING' && httpRequest.body.string) {
body = httpRequest.body.string;
} else if (httpRequest.body) {
const str =
typeof httpRequest.body !== 'string' ? JSON.stringify(httpRequest.body) : httpRequest.body;
body = sanitizeString(str, toSanitize);
}
return body;
};
@ -340,8 +348,13 @@ const transformRecordedData = (expectation, toSanitize) => {
encoding: 'base64',
content: httpResponse.body.base64Bytes,
};
} else if (httpResponse.body) {
responseBody = httpResponse.body;
} else if (httpResponse.body && httpResponse.body.json) {
responseBody = JSON.stringify(httpResponse.body.json);
} else {
responseBody =
typeof httpResponse.body === 'string'
? httpResponse.body
: httpResponse.body && JSON.stringify(httpResponse.body);
}
// replace recorded user with fake one

View File

@ -216,7 +216,11 @@ const transformRecordedData = (expectation, toSanitize) => {
const requestBodySanitizer = httpRequest => {
let body;
if (httpRequest.body && httpRequest.body.type === 'JSON' && httpRequest.body.json) {
const bodyObject = JSON.parse(httpRequest.body.json);
const bodyObject =
typeof httpRequest.body.json === 'string'
? JSON.parse(httpRequest.body.json)
: httpRequest.body.json;
if (bodyObject.encoding === 'base64') {
// sanitize encoded data
const decodedBody = Buffer.from(bodyObject.content, 'base64').toString('binary');
@ -225,10 +229,14 @@ const transformRecordedData = (expectation, toSanitize) => {
bodyObject.content = sanitizedEncodedContent;
body = JSON.stringify(bodyObject);
} else {
body = httpRequest.body.json;
body = JSON.stringify(bodyObject);
}
} else if (httpRequest.body && httpRequest.body.type === 'STRING' && httpRequest.body.string) {
body = sanitizeString(httpRequest.body.string, toSanitize);
} else if (httpRequest.body) {
const str =
typeof httpRequest.body !== 'string' ? JSON.stringify(httpRequest.body) : httpRequest.body;
body = sanitizeString(str, toSanitize);
}
return body;
};
@ -246,8 +254,13 @@ const transformRecordedData = (expectation, toSanitize) => {
encoding: 'base64',
content: httpResponse.body.base64Bytes,
};
} else if (httpResponse.body) {
responseBody = httpResponse.body;
} else if (httpResponse.body && httpResponse.body.json) {
responseBody = JSON.stringify(httpResponse.body.json);
} else {
responseBody =
typeof httpResponse.body === 'string'
? httpResponse.body
: httpResponse.body && JSON.stringify(httpResponse.body);
}
// replace recorded user with fake one

View File

@ -91,6 +91,18 @@ function goToMediaLibrary() {
cy.contains('button', 'Media').click();
}
function assertUnpublishedEntryInEditor() {
cy.contains('button', 'Delete unpublished entry');
}
function assertPublishedEntryInEditor() {
cy.contains('button', 'Delete published entry');
}
function assertUnpublishedChangesInEditor() {
cy.contains('button', 'Delete unpublished changes');
}
function goToEntry(entry) {
goToCollections();
cy.get('a h2')
@ -252,12 +264,17 @@ function populateEntry(entry, onDone = flushClockAndSave) {
const value = entry[key];
if (key === 'body') {
cy.getMarkdownEditor()
.first()
.click()
.clear({ force: true })
.type(value, { force: true });
} else {
cy.get(`[id^="${key}-field"]`).clear({ force: true });
cy.get(`[id^="${key}-field"]`).type(value, { force: true });
cy.get(`[id^="${key}-field"]`)
.first()
.clear({ force: true });
cy.get(`[id^="${key}-field"]`)
.first()
.type(value, { force: true });
}
}
@ -305,7 +322,8 @@ function publishEntry({ createNew = false, duplicate = false } = {}) {
selectDropdownItem('Publish', publishTypes.publishNow);
}
assertNotification(notifications.saved);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
});
}
@ -686,4 +704,8 @@ module.exports = {
publishAndDuplicateEntryInEditor,
assertNotification,
assertFieldValidationError,
flushClockAndSave,
assertPublishedEntryInEditor,
assertUnpublishedEntryInEditor,
assertUnpublishedChangesInEditor,
};