Feat: multi content authoring (#4139)
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
59
cypress/integration/common/i18n.js
Normal file
59
cypress/integration/common/i18n.js
Normal 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();
|
||||
};
|
54
cypress/integration/common/i18n_editorial_workflow_spec.js
Normal file
54
cypress/integration/common/i18n_editorial_workflow_spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
@ -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 });
|
||||
});
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
|
Reference in New Issue
Block a user