This commit is contained in:
2025-08-25 20:24:23 +08:00
parent 30106e0129
commit 0ae8d7a709
1044 changed files with 321581 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
import '../../utils/dismiss-local-backup';
import {
login,
createPost,
createPostAndExit,
updateExistingPostAndExit,
exitEditor,
goToWorkflow,
goToCollections,
updateWorkflowStatus,
publishWorkflowEntry,
assertWorkflowStatusInEditor,
assertPublishedEntry,
deleteEntryInEditor,
assertOnCollectionsPage,
assertEntryDeleted,
assertWorkflowStatus,
updateWorkflowStatusInEditor,
} from '../../utils/steps';
import { workflowStatus, editorStatus } from '../../utils/constants';
export default function({ entries, getUser }) {
it('successfully loads', () => {
login(getUser());
});
it('can create an entry', () => {
login(getUser());
createPostAndExit(entries[0]);
});
it('can update an entry', () => {
login(getUser());
createPostAndExit(entries[0]);
updateExistingPostAndExit(entries[0], entries[1]);
});
it('can publish an editorial workflow entry', () => {
login(getUser());
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entries[0]);
});
it('can change workflow status', () => {
login(getUser());
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
updateWorkflowStatus(entries[0], workflowStatus.review, workflowStatus.ready);
updateWorkflowStatus(entries[0], workflowStatus.ready, workflowStatus.review);
updateWorkflowStatus(entries[0], workflowStatus.review, workflowStatus.draft);
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
});
it('can change status on and publish multiple entries', () => {
login(getUser());
createPostAndExit(entries[0]);
createPostAndExit(entries[1]);
createPostAndExit(entries[2]);
goToWorkflow();
updateWorkflowStatus(entries[2], workflowStatus.draft, workflowStatus.ready);
updateWorkflowStatus(entries[1], workflowStatus.draft, workflowStatus.ready);
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entries[2]);
publishWorkflowEntry(entries[1]);
publishWorkflowEntry(entries[0]);
goToCollections();
assertPublishedEntry([entries[2], entries[1], entries[0]]);
});
it('can delete an entry', () => {
login(getUser());
createPost(entries[0]);
deleteEntryInEditor();
assertOnCollectionsPage();
assertEntryDeleted(entries[0]);
});
it('can update workflow status from within the editor', () => {
login(getUser());
createPost(entries[0]);
assertWorkflowStatusInEditor(editorStatus.draft);
updateWorkflowStatusInEditor(editorStatus.review);
assertWorkflowStatusInEditor(editorStatus.review);
updateWorkflowStatusInEditor(editorStatus.ready);
assertWorkflowStatusInEditor(editorStatus.ready);
exitEditor();
goToWorkflow();
assertWorkflowStatus(entries[0], workflowStatus.ready);
});
}

View File

@@ -0,0 +1,51 @@
import '../../utils/dismiss-local-backup';
import {
login,
createPostAndExit,
goToWorkflow,
goToCollections,
updateWorkflowStatus,
publishWorkflowEntry,
assertPublishedEntry,
} from '../../utils/steps';
import { workflowStatus } from '../../utils/constants';
const versions = ['2.9.7', '2.10.24'];
export default function({ entries, getUser }) {
versions.forEach(version => {
it(`migrate from ${version} to latest`, () => {
cy.task('switchToVersion', {
version,
});
cy.reload();
login(getUser());
createPostAndExit(entries[0]);
createPostAndExit(entries[1]);
createPostAndExit(entries[2]);
goToWorkflow();
updateWorkflowStatus(entries[2], workflowStatus.draft, workflowStatus.ready);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1500); // older versions of the CMS didn't wait fully for the update to be resolved
updateWorkflowStatus(entries[1], workflowStatus.draft, workflowStatus.ready);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1500); // older versions of the CMS didn't wait fully for the update to be resolved
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1500); // older versions of the CMS didn't wait fully for the update to be resolved
cy.task('switchToVersion', {
version: 'latest',
});
cy.reload();
// allow migration code to run for 5 minutes
publishWorkflowEntry(entries[2], 5 * 60 * 1000);
publishWorkflowEntry(entries[1]);
publishWorkflowEntry(entries[0]);
goToCollections();
assertPublishedEntry([entries[2], entries[1], entries[0]]);
});
});
}

View File

@@ -0,0 +1,21 @@
export const entry1 = {
title: 'first title',
body: 'first body',
description: 'first description',
category: 'first category',
tags: 'tag1',
};
export const entry2 = {
title: 'second title',
body: 'second body',
description: 'second description',
category: 'second category',
tags: 'tag2',
};
export const entry3 = {
title: 'third title',
body: 'third body',
description: 'third description',
category: 'third category',
tags: 'tag3',
};

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');
});
cy.contains('button', 'Save').click();
};
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,177 @@
import '../../utils/dismiss-local-backup';
import {
login,
goToMediaLibrary,
newPost,
populateEntry,
exitEditor,
goToWorkflow,
updateWorkflowStatus,
publishWorkflowEntry,
goToEntry,
goToCollections,
} from '../../utils/steps';
import { workflowStatus } from '../../utils/constants';
function uploadMediaFile() {
assertNoImagesInLibrary();
const fixture = 'cypress/fixtures/media/netlify.png';
cy.get('input[type="file"]').selectFile(fixture, { force: true });
cy.contains('span', 'Uploading...').should('not.exist');
assertImagesInLibrary();
}
function assertImagesInLibrary() {
cy.get('img[class*="CardImage"]').should('exist');
}
function assertNoImagesInLibrary() {
cy.get('h1')
.contains('Loading...')
.should('not.exist');
cy.get('img[class*="CardImage"]').should('not.exist');
}
function deleteImage() {
cy.get('img[class*="CardImage"]').click();
cy.contains('button', 'Delete selected').click();
assertNoImagesInLibrary();
}
function chooseSelectedMediaFile() {
cy.contains('button', 'Choose selected').should('not.be.disabled');
cy.contains('button', 'Choose selected').click();
}
function chooseAnImage() {
cy.contains('button', 'Choose an image').click();
}
function waitForEntryToLoad() {
cy.contains('button', 'Saving...').should('not.exist');
cy.clock().tick(5000);
cy.contains('div', 'Loading entry...').should('not.exist');
}
function matchImageSnapshot() {
// cy.matchImageSnapshot();
}
function newPostAndUploadImage() {
newPost();
chooseAnImage();
uploadMediaFile();
}
function newPostWithImage(entry) {
newPostAndUploadImage();
chooseSelectedMediaFile();
populateEntry(entry);
waitForEntryToLoad();
}
function publishPostWithImage(entry) {
newPostWithImage(entry);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
exitEditor();
goToWorkflow();
updateWorkflowStatus(entry, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry);
}
function closeMediaLibrary() {
cy.get('button[class*="CloseButton"]').click();
}
function switchToGridView() {
cy.get('div[class*="ViewControls"]').within(() => {
cy.get('button')
.last()
.click();
});
}
function assertGridEntryImage(entry) {
cy.contains('li', entry.title).within(() => {
cy.get('div[class*="CardImage"]').should('be.visible');
});
}
export default function({ entries, getUser }) {
beforeEach(() => {
login(getUser && getUser());
});
it('can upload image from global media library', () => {
goToMediaLibrary();
uploadMediaFile();
matchImageSnapshot();
closeMediaLibrary();
});
it('can delete image from global media library', () => {
goToMediaLibrary();
uploadMediaFile();
closeMediaLibrary();
goToMediaLibrary();
deleteImage();
matchImageSnapshot();
closeMediaLibrary();
});
it('can upload image from entry media library', () => {
newPostAndUploadImage();
matchImageSnapshot();
closeMediaLibrary();
exitEditor();
});
it('can save entry with image', () => {
newPostWithImage(entries[0]);
matchImageSnapshot();
exitEditor();
});
it('can publish entry with image', () => {
publishPostWithImage(entries[0]);
goToEntry(entries[0]);
waitForEntryToLoad();
matchImageSnapshot();
});
it('should not show draft entry image in global media library', () => {
newPostWithImage(entries[0]);
cy.clock().then(clock => {
if (clock) {
clock.tick(150);
clock.tick(150);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
}
exitEditor();
goToMediaLibrary();
assertNoImagesInLibrary();
matchImageSnapshot();
});
});
it('should show published entry image in global media library', () => {
publishPostWithImage(entries[0]);
cy.clock().tick();
goToMediaLibrary();
assertImagesInLibrary();
matchImageSnapshot();
});
it('should show published entry image in grid view', () => {
publishPostWithImage(entries[0]);
goToCollections();
switchToGridView();
assertGridEntryImage(entries[0]);
matchImageSnapshot();
});
}

View File

@@ -0,0 +1,76 @@
import '../../utils/dismiss-local-backup';
import {
login,
createPostAndExit,
updateExistingPostAndExit,
goToWorkflow,
deleteWorkflowEntry,
updateWorkflowStatus,
publishWorkflowEntry,
} from '../../utils/steps';
import { workflowStatus } from '../../utils/constants';
export default function({ entries, getUser, getForkUser }) {
it('successfully loads', () => {
login(getUser());
});
it('can create an entry', () => {
login(getUser());
createPostAndExit(entries[0]);
});
it('can update an entry', () => {
login(getUser());
createPostAndExit(entries[0]);
updateExistingPostAndExit(entries[0], entries[1]);
});
it('can publish an editorial workflow entry', () => {
login(getUser());
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entries[0]);
});
it('successfully forks repository and loads', () => {
login(getForkUser());
});
it('can create an entry on fork', () => {
login(getForkUser());
createPostAndExit(entries[0]);
});
it('can update a draft entry on fork', () => {
login(getForkUser());
createPostAndExit(entries[0]);
updateExistingPostAndExit(entries[0], entries[1]);
});
it('can change entry status from fork', () => {
login(getForkUser());
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
});
it('can delete review entry from fork', () => {
login(getForkUser());
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
deleteWorkflowEntry(entries[0]);
});
it('can return entry to draft and delete it', () => {
login(getForkUser());
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
updateWorkflowStatus(entries[0], workflowStatus.review, workflowStatus.draft);
deleteWorkflowEntry(entries[0]);
});
}

View File

@@ -0,0 +1,14 @@
import '../../utils/dismiss-local-backup';
import { login, createPostAndPublish, assertPublishedEntry } from '../../utils/steps';
export default function({ entries, getUser }) {
it('successfully loads', () => {
login(getUser());
});
it('can create an entry', () => {
login(getUser());
createPostAndPublish(entries[0]);
assertPublishedEntry(entries[0]);
});
}

View File

@@ -0,0 +1,65 @@
export const before = (taskResult, options, backend) => {
Cypress.config('taskTimeout', 7 * 60 * 1000);
cy.task('setupBackend', { backend, options }).then(data => {
taskResult.data = data;
Cypress.config('defaultCommandTimeout', data.mockResponses ? 5 * 1000 : 1 * 60 * 1000);
});
};
export const after = (taskResult, backend) => {
cy.task('teardownBackend', {
backend,
...taskResult.data,
});
};
export const beforeEach = (taskResult, backend) => {
const spec = Cypress.mocha.getRunner().suite.ctx.currentTest.parent.title;
const testName = Cypress.mocha.getRunner().suite.ctx.currentTest.title;
cy.task('setupBackendTest', {
backend,
...taskResult.data,
spec,
testName,
});
if (taskResult.data.mockResponses) {
const fixture = `${spec}__${testName}.json`;
console.log('loading fixture:', fixture);
cy.stubFetch({ fixture });
}
return cy.clock(0, ['Date']);
};
export const afterEach = (taskResult, backend) => {
const spec = Cypress.mocha.getRunner().suite.ctx.currentTest.parent.title;
const testName = Cypress.mocha.getRunner().suite.ctx.currentTest.title;
cy.task('teardownBackendTest', {
backend,
...taskResult.data,
spec,
testName,
});
if (!process.env.RECORD_FIXTURES) {
const {
suite: {
ctx: {
currentTest: { state, _retries: retries, _currentRetry: currentRetry },
},
},
} = Cypress.mocha.getRunner();
if (state === 'failed' && retries === currentRetry) {
Cypress.runner.stop();
}
}
};
export const seedRepo = (taskResult, backend) => {
cy.task('seedRepo', {
backend,
...taskResult.data,
});
};

View File

@@ -0,0 +1,30 @@
import fixture from './common/editorial_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'bitbucket';
describe('BitBucket Backend Editorial Workflow', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow' }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,31 @@
import fixture from './common/editorial_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'git-gateway';
const provider = 'github';
describe('Git Gateway (GitHub) Backend Editorial Workflow', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', provider }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,31 @@
import fixture from './common/editorial_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'git-gateway';
const provider = 'gitlab';
describe('Git Gateway (GitLab) Backend Editorial Workflow', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', provider }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,37 @@
import fixture from './common/editorial_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'github';
describe('GitHub Backend Editorial Workflow - GraphQL API', () => {
const taskResult = { data: {} };
before(() => {
specUtils.before(
taskResult,
{
backend: { use_graphql: true, open_authoring: false },
publish_mode: 'editorial_workflow',
},
backend,
);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,38 @@
import fixture from './common/open_authoring';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'github';
describe('GitHub Backend Editorial Workflow - GraphQL API - Open Authoring', () => {
const taskResult = { data: {} };
before(() => {
specUtils.before(
taskResult,
{
backend: { use_graphql: true, open_authoring: true },
publish_mode: 'editorial_workflow',
},
backend,
);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
getForkUser: () => taskResult.data.forkUser,
});
});

View File

@@ -0,0 +1,37 @@
import fixture from './common/editorial_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'github';
describe('GitHub Backend Editorial Workflow - REST API', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(
taskResult,
{
backend: { use_graphql: false, open_authoring: false },
publish_mode: 'editorial_workflow',
},
backend,
);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,38 @@
import fixture from './common/open_authoring';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'github';
describe('GitHub Backend Editorial Workflow - REST API - Open Authoring', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(
taskResult,
{
backend: { use_graphql: false, open_authoring: true },
publish_mode: 'editorial_workflow',
},
backend,
);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
getForkUser: () => taskResult.data.forkUser,
});
});

View File

@@ -0,0 +1,36 @@
import fixture from './common/editorial_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'gitlab';
describe('GitLab Backend Editorial Workflow', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow' }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
if (
Cypress.mocha.getRunner().suite.ctx.currentTest.title ===
'can change status on and publish multiple entries'
) {
Cypress.mocha.getRunner().suite.ctx.currentTest.skip();
}
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,32 @@
import fixture from './common/editorial_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'proxy';
const mode = 'git';
describe.skip(`Proxy Backend Editorial Workflow - '${mode}' mode`, () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', mode }, backend);
Cypress.config('defaultCommandTimeout', 5 * 1000);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,312 @@
import '../utils/dismiss-local-backup';
import {
login,
createPost,
createPostAndExit,
exitEditor,
goToWorkflow,
goToCollections,
updateWorkflowStatus,
publishWorkflowEntry,
assertWorkflowStatusInEditor,
assertPublishedEntry,
deleteEntryInEditor,
assertOnCollectionsPage,
assertEntryDeleted,
assertWorkflowStatus,
updateWorkflowStatusInEditor,
unpublishEntry,
publishEntryInEditor,
duplicateEntry,
goToEntry,
populateEntry,
publishAndCreateNewEntryInEditor,
publishAndDuplicateEntryInEditor,
assertNotification,
assertFieldValidationError,
} from '../utils/steps';
import { workflowStatus, editorStatus, publishTypes, notifications } from '../utils/constants';
const entry1 = {
title: 'first title',
body: 'first body',
};
const entry2 = {
title: 'second title',
body: 'second body',
};
const entry3 = {
title: 'third title',
body: 'third body',
};
describe('Test Backend Editorial Workflow', () => {
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.task('updateConfig', { collections: [{ publish: true }] });
});
it('successfully loads', () => {
login();
});
it('can create an entry', () => {
login();
createPost(entry1);
// new entry should show 'Delete unpublished entry'
cy.contains('button', 'Delete unpublished entry');
cy.url().should(
'eq',
`http://localhost:8080/#/collections/posts/entries/1970-01-01-${entry1.title
.toLowerCase()
.replace(/\s/, '-')}`,
);
exitEditor();
});
it('can publish an editorial workflow entry', () => {
login();
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry1);
});
it('can update an entry', () => {
login();
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry1);
goToEntry(entry1);
populateEntry(entry2);
// existing entry should show 'Delete unpublished changes'
cy.contains('button', 'Delete unpublished changes');
// existing entry slug should remain the same after save'
cy.url().should(
'eq',
`http://localhost:8080/#/collections/posts/entries/1970-01-01-${entry1.title
.toLowerCase()
.replace(/\s/, '-')}`,
);
exitEditor();
});
it('can change workflow status', () => {
login();
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.ready);
updateWorkflowStatus(entry1, workflowStatus.ready, workflowStatus.review);
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
});
it('can change status on and publish multiple entries', () => {
login();
createPostAndExit(entry1);
createPostAndExit(entry2);
createPostAndExit(entry3);
goToWorkflow();
updateWorkflowStatus(entry3, workflowStatus.draft, workflowStatus.ready);
updateWorkflowStatus(entry2, workflowStatus.draft, workflowStatus.ready);
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry3);
publishWorkflowEntry(entry2);
publishWorkflowEntry(entry1);
goToCollections();
assertPublishedEntry([entry3, entry2, entry1]);
});
it('can delete an entry', () => {
login();
createPost(entry1);
deleteEntryInEditor();
assertOnCollectionsPage();
assertEntryDeleted(entry1);
});
it('can update workflow status from within the editor', () => {
login();
createPost(entry1);
assertWorkflowStatusInEditor(editorStatus.draft);
updateWorkflowStatusInEditor(editorStatus.review);
assertWorkflowStatusInEditor(editorStatus.review);
updateWorkflowStatusInEditor(editorStatus.ready);
assertWorkflowStatusInEditor(editorStatus.ready);
exitEditor();
goToWorkflow();
assertWorkflowStatus(entry1, workflowStatus.ready);
});
it('can unpublish an existing entry', () => {
// first publish an entry
login();
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry1);
// then unpublish it
unpublishEntry(entry1);
});
it('can duplicate an existing entry', () => {
login();
createPost(entry1);
updateWorkflowStatusInEditor(editorStatus.ready);
publishEntryInEditor(publishTypes.publishNow);
duplicateEntry(entry1);
});
it('cannot publish when "publish" is false', () => {
cy.task('updateConfig', { collections: [{ publish: false }] });
login();
createPost(entry1);
cy.contains('span', 'Publish').should('not.exist');
exitEditor();
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
cy.contains('button', 'Publish new entry').should('not.exist');
});
it('can create a new entry, publish and create new', () => {
login();
createPost(entry1);
updateWorkflowStatusInEditor(editorStatus.ready);
publishAndCreateNewEntryInEditor(entry1);
});
it('can create a new entry, publish and duplicate', () => {
login();
createPost(entry1);
updateWorkflowStatusInEditor(editorStatus.ready);
publishAndDuplicateEntryInEditor(entry1);
});
const inSidebar = func => {
cy.get('[class*=SidebarNavList]').within(func);
};
const inGrid = func => {
cy.get('[class*=CardsGrid]').within(func);
};
it('can access nested collection items', () => {
login();
inSidebar(() => cy.contains('a', 'Pages').click());
inSidebar(() => cy.contains('a', /^Directory$/));
inGrid(() => cy.contains('a', 'Root Page'));
inSidebar(() => cy.contains('a', /^Directory$/).click());
inSidebar(() => cy.contains('a', /^Sub Directory$/));
inSidebar(() => cy.contains('a', 'Another Sub Directory'));
inSidebar(() => cy.contains('a', /^Sub Directory$/).click());
inSidebar(() => cy.contains('a', 'Nested Directory'));
cy.url().should(
'eq',
'http://localhost:8080/#/collections/pages/filter/directory/sub-directory',
);
inSidebar(() => cy.contains('a', 'Pages').click());
inSidebar(() => cy.contains('a', 'Pages').click());
inGrid(() => cy.contains('a', 'Another Sub Directory').should('not.exist'));
});
it('can navigate to nested entry', () => {
login();
inSidebar(() => cy.contains('a', 'Pages').click());
inSidebar(() => cy.contains('a', /^Directory$/).click());
inSidebar(() => cy.contains('a', 'Another Sub Directory').click());
inGrid(() => cy.contains('a', 'Another Sub Directory'));
});
it(`can create a new entry with custom path`, () => {
login();
inSidebar(() => cy.contains('a', 'Pages').click());
inSidebar(() => cy.contains('a', /^Directory$/).click());
inSidebar(() => cy.contains('a', /^Sub Directory$/).click());
cy.contains('a', 'New Page').click();
cy.get('[id^="path-field"]').should('have.value', 'directory/sub-directory');
cy.get('[id^="path-field"]').type('/new-path');
cy.get('[id^="title-field"]').type('New Path Title');
cy.clock().then(clock => {
clock.tick(150);
});
cy.contains('button', 'Save').click();
assertNotification(notifications.saved);
updateWorkflowStatusInEditor(editorStatus.ready);
publishEntryInEditor(publishTypes.publishNow);
exitEditor();
inSidebar(() => cy.contains('a', 'New Path Title'));
inSidebar(() => cy.contains('a', /^Directory$/).click());
inSidebar(() => cy.contains('a', /^Directory$/).click());
inGrid(() => cy.contains('a', 'New Path Title').should('not.exist'));
});
it(`can't create an entry with an existing path`, () => {
login();
inSidebar(() => cy.contains('a', 'Pages').click());
inSidebar(() => cy.contains('a', /^Directory$/).click());
inSidebar(() => cy.contains('a', /^Sub Directory$/).click());
cy.contains('a', 'New Page').click();
cy.get('[id^="title-field"]').type('New Path Title');
cy.clock().then(clock => {
clock.tick(150);
});
cy.contains('button', 'Save').click();
assertFieldValidationError({
message: `Path 'directory/sub-directory' already exists`,
fieldLabel: 'Path',
});
});
it('can move an existing entry to a new path', () => {
login();
inSidebar(() => cy.contains('a', 'Pages').click());
inSidebar(() => cy.contains('a', /^Directory$/).click());
inGrid(() => cy.contains('a', /^Directory$/).click());
cy.get('[id^="path-field"]').should('have.value', 'directory');
cy.get('[id^="path-field"]').clear();
cy.get('[id^="path-field"]').type('new-directory');
cy.get('[id^="title-field"]').clear();
cy.get('[id^="title-field"]').type('New Directory');
cy.clock().then(clock => {
clock.tick(150);
});
cy.contains('button', 'Save').click();
assertNotification(notifications.saved);
updateWorkflowStatusInEditor(editorStatus.ready);
publishEntryInEditor(publishTypes.publishNow);
exitEditor();
inSidebar(() => cy.contains('a', 'New Directory').click());
inSidebar(() => cy.contains('a', /^Sub Directory$/));
inSidebar(() => cy.contains('a', 'Another Sub Directory'));
});
});

View File

@@ -0,0 +1,109 @@
import '../utils/dismiss-local-backup';
import {
login,
validateObjectFieldsAndExit,
validateNestedObjectFieldsAndExit,
validateListFieldsAndExit,
validateNestedListFieldsAndExit,
} from '../utils/steps';
import { setting1, setting2 } from '../utils/constants';
const nestedListConfig = {
collections: [
{},
{},
{
name: 'settings',
label: 'Settings',
editor: { preview: false },
files: [
{},
{},
{
name: 'hotel_locations',
label: 'Hotel Locations',
file: '_data/hotel_locations.yml',
fields: [
{
label: 'Country',
name: 'country',
widget: 'string',
},
{
label: 'Hotel Locations',
name: 'hotel_locations',
widget: 'list',
fields: [
{
label: 'Cities',
name: 'cities',
widget: 'list',
fields: [
{
label: 'City',
name: 'city',
widget: 'string',
},
{
label: 'Number of Hotels in City',
name: 'number_of_hotels_in_city',
widget: 'number',
},
{
label: 'City Locations',
name: 'city_locations',
widget: 'list',
fields: [
{
label: 'Hotel Name',
name: 'hotel_name',
widget: 'string',
},
],
},
],
},
],
},
],
},
],
},
],
};
describe('Test Backend Editorial Workflow', () => {
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
});
beforeEach(() => {
cy.task('setupBackend', { backend: 'test' });
});
it('can validate object fields', () => {
login();
validateObjectFieldsAndExit(setting1);
});
it('can validate fields nested in an object field', () => {
login();
validateNestedObjectFieldsAndExit(setting1);
});
it('can validate list fields', () => {
login();
validateListFieldsAndExit(setting2);
});
it('can validate deeply nested list fields', () => {
cy.task('updateConfig', nestedListConfig);
login();
validateNestedListFieldsAndExit(setting2);
});
});

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-01T01:00
description: first description
category: first category
tags:
- tag1
---
`;
const expectedDeContent = `---
title: de
date: 1970-01-01T01:00
---
`;
const expectedFrContent = `---
title: fr
date: 1970-01-01T01:00
---
`;
const contentSingleFile = `---
en:
template: post
date: 1970-01-01T01:00
title: first title
body: first body
description: first description
category: first category
tags:
- tag1
de:
date: 1970-01-01T01:00
title: de
fr:
date: 1970-01-01T01:00
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

@@ -0,0 +1,94 @@
import '../utils/dismiss-local-backup';
describe('Markdown widget', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.loginAndNewPost();
cy.clearMarkdownEditorContent();
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
// describe('pressing backspace', () => {
it('sets non-default block to default when empty', () => {
cy.focused()
.clickHeadingOneButton()
.backspace()
.confirmMarkdownEditorContent(`
<p></p>
`);
});
it('moves to previous block when no character left to delete', () => {
cy.focused()
.type('foo')
.enter()
.clickHeadingOneButton()
.type('a')
.backspace({times: 2})
.confirmMarkdownEditorContent(`
<p>foo</p>
`);
});
it('does nothing at start of first block in document when non-empty and non-default', () => {
cy.focused()
.clickHeadingOneButton()
.type('foo')
.setCursorBefore('foo')
.backspace({ times: 4 })
.confirmMarkdownEditorContent(`
<h1>foo</h1>
`);
});
it('deletes individual characters in middle of non-empty non-default block in document', () => {
cy.focused()
.clickHeadingOneButton()
.type('foo')
.setCursorAfter('fo')
.backspace({ times: 3 })
.confirmMarkdownEditorContent(`
<h1>o</h1>
`);
});
it('at beginning of non-first block, moves default block content to previous block', () => {
cy.focused()
.clickHeadingOneButton()
.type('foo')
.enter()
.type('bar')
.setCursorBefore('bar')
.backspace()
.confirmMarkdownEditorContent(`
<h1>foobar</h1>
`);
});
it('at beginning of non-first block, moves non-default block content to previous block', () => {
cy.focused()
.type('foo')
.enter()
.clickHeadingOneButton()
.type('bar')
.enter()
.clickHeadingTwoButton()
.type('baz')
.setCursorBefore('baz')
.backspace()
.confirmMarkdownEditorContent(`
<p>foo</p>
<h1>barbaz</h1>
`)
.setCursorBefore('bar')
.backspace()
.confirmMarkdownEditorContent(`
<p>foobarbaz</p>
`);
// });
});
});

View File

@@ -0,0 +1,136 @@
import { oneLineTrim, stripIndent } from 'common-tags';
import '../utils/dismiss-local-backup';
describe('Markdown widget code block', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.loginAndNewPost();
cy.clearMarkdownEditorContent();
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
describe('code block', () => {
it('outputs code', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy
.insertCodeBlock()
.type('foo')
.enter()
.type('bar')
.confirmMarkdownEditorContent(
`
${codeBlock(`
foo
bar
`)}
`,
)
.wait(500)
.clickModeToggle().confirmMarkdownEditorContent(`
${codeBlockRaw(`
foo
bar
`)}
`);
});
});
});
function codeBlockRaw(content) {
return ['```', ...stripIndent(content).split('\n'), '```']
.map(
line => oneLineTrim`
<div>
<span>
<span>
<span>${line}</span>
</span>
</span>
</div>
`,
)
.join('');
}
function codeBlock(content) {
const lines = stripIndent(content)
.split('\n')
.map(
(line, idx) => `
<div>
<div>
<div>${idx + 1}</div>
</div>
<pre><span>${line}</span></pre>
</div>
`,
)
.join('');
return oneLineTrim`
<div>
<div></div>
<div>
<div>
<div><label>Code Block</label></div>
<div><button><span><svg>
<path></path>
</svg></span></button>
<div>
<div>
<div><textarea></textarea></div>
<div>
<div></div>
</div>
<div>
<div></div>
</div>
<div></div>
<div></div>
<div>
<div>
<div>
<div>
<div>
<div>
<pre><span>xxxxxxxxxx</span></pre>
</div>
<div></div>
<div></div>
<div>
<div> </div>
</div>
<div>
${lines}
</div>
</div>
</div>
</div>
</div>
<div></div>
<div>
<div></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<span>
<span>
<span></span>
</span>
</span>
</div>
</div>
<div></div>
</div>
`;
}

View File

@@ -0,0 +1,109 @@
import '../utils/dismiss-local-backup';
describe('Markdown widget breaks', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.loginAndNewPost();
cy.clearMarkdownEditorContent();
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
describe('pressing enter', () => {
it('creates new default block from empty block', () => {
cy.focused()
.enter()
.confirmMarkdownEditorContent(`
<p></p>
<p></p>
`);
});
it('creates new default block when selection collapsed at end of block', () => {
cy.focused()
.type('foo')
.enter()
.confirmMarkdownEditorContent(`
<p>foo</p>
<p></p>
`);
});
it('creates new default block when selection collapsed at end of non-default block', () => {
cy.clickHeadingOneButton()
.type('foo')
.enter()
.confirmMarkdownEditorContent(`
<h1>foo</h1>
<p></p>
`);
});
it('creates new default block when selection collapsed in empty non-default block', () => {
cy.clickHeadingOneButton()
.enter()
.confirmMarkdownEditorContent(`
<h1></h1>
<p></p>
`);
});
it('splits block into two same-type blocks when collapsed selection at block start', () => {
cy.clickHeadingOneButton()
.type('foo')
.setCursorBefore('foo')
.enter()
.confirmMarkdownEditorContent(`
<h1></h1>
<h1>foo</h1>
`);
});
it('splits block into two same-type blocks when collapsed in middle of selection at block start', () => {
cy.clickHeadingOneButton()
.type('foo')
.setCursorBefore('oo')
.enter()
.confirmMarkdownEditorContent(`
<h1>f</h1>
<h1>oo</h1>
`);
});
it('deletes selected content and splits to same-type block when selection is expanded', () => {
cy.clickHeadingOneButton()
.type('foo bar')
.setSelection('o b')
.enter()
.confirmMarkdownEditorContent(`
<h1>fo</h1>
<h1>ar</h1>
`);
});
});
describe('pressing shift+enter', () => {
it('creates line break', () => {
cy.focused()
.enter({ shift: true })
.confirmMarkdownEditorContent(`
<p>
<br>
</p>
`);
});
it('creates consecutive line break', () => {
cy.focused()
.enter({ shift: true, times: 4 })
.confirmMarkdownEditorContent(`
<p>
<br>
<br>
<br>
<br>
</p>
`);
});
});
});

View File

@@ -0,0 +1,106 @@
import '../utils/dismiss-local-backup';
import {HOT_KEY_MAP} from "../utils/constants";
const headingNumberToWord = ['', 'one', 'two', 'three', 'four', 'five', 'six'];
const isMac = Cypress.platform === 'darwin';
const modifierKey = isMac ? '{meta}' : '{ctrl}';
// eslint-disable-next-line func-style
const replaceMod = (str) => str.replace(/mod\+/g, modifierKey).replace(/shift\+/g, '{shift}');
describe('Markdown widget hotkeys', () => {
describe('hot keys', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.loginAndNewPost();
cy.clearMarkdownEditorContent();
cy.focused()
.type('foo')
.setSelection('foo').as('selection');
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
describe('bold', () => {
it('pressing mod+b bolds the text', () => {
cy.get('@selection')
.type(replaceMod(HOT_KEY_MAP['bold']))
.confirmMarkdownEditorContent(`
<p>
<strong>foo</strong>
</p>
`)
.type(replaceMod(HOT_KEY_MAP['bold']));
});
});
describe('italic', () => {
it('pressing mod+i italicizes the text', () => {
cy.get('@selection')
.type(replaceMod(HOT_KEY_MAP['italic']))
.confirmMarkdownEditorContent(`
<p>
<em>foo</em>
</p>
`)
.type(replaceMod(HOT_KEY_MAP['italic']));
});
});
describe('strikethrough', () => {
it('pressing mod+shift+s displays a strike through the text', () => {
cy.get('@selection')
.type(replaceMod(HOT_KEY_MAP['strikethrough']))
.confirmMarkdownEditorContent(`
<p>
<s>foo</s>
</p>
`).type(replaceMod(HOT_KEY_MAP['strikethrough']));
});
});
describe('code', () => {
it('pressing mod+shift+c displays a code block around the text', () => {
cy.get('@selection')
.type(replaceMod(HOT_KEY_MAP['code']))
.confirmMarkdownEditorContent(`
<p>
<code>foo</code>
</p>
`).type(replaceMod(HOT_KEY_MAP['code']));
});
});
describe('link', () => {
before(() => {
});
it('pressing mod+k transforms the text to a link', () => {
cy.window().then((win) => {
cy.get('@selection')
.type(replaceMod(HOT_KEY_MAP['link']))
cy.stub(win, 'prompt').returns('https://google.com');
cy.confirmMarkdownEditorContent('<p><a>foo</a></p>')
.type(replaceMod(HOT_KEY_MAP['link']));
});
});
});
describe('headings', () => {
for (let i = 1; i <= 6; i++) {
it(`pressing mod+${i} transforms the text to a heading`, () => {
cy.get('@selection')
.type(replaceMod(HOT_KEY_MAP[`heading-${headingNumberToWord[i]}`]))
.confirmMarkdownEditorContent(`<h${i}>foo</h${i}>`)
.type(replaceMod(HOT_KEY_MAP[`heading-${headingNumberToWord[i]}`]))
});
}
});
});
});

View File

@@ -0,0 +1,70 @@
import '../utils/dismiss-local-backup';
describe('Markdown widget link', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.loginAndNewPost();
cy.clearMarkdownEditorContent();
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
describe('link', () => {
it('can add a new valid link', () => {
const link = 'https://www.decapcms.org/';
cy.window().then(win => {
cy.stub(win, 'prompt').returns(link);
});
cy.focused().clickLinkButton();
cy.confirmMarkdownEditorContent(`<p><a>${link}</a></p>`);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(300);
cy.clickModeToggle();
cy.confirmRawEditorContent(`<${link}>`);
});
it('can add a new invalid link', () => {
const link = 'www.decapcms.org';
cy.window().then(win => {
cy.stub(win, 'prompt').returns(link);
});
cy.focused().clickLinkButton();
cy.confirmMarkdownEditorContent(`<p><a>${link}</a></p>`);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(300);
cy.clickModeToggle();
cy.confirmRawEditorContent(`[${link}](${link})`);
});
it('can select existing text as link', () => {
const link = 'https://www.decapcms.org';
cy.window().then(win => {
cy.stub(win, 'prompt').returns(link);
});
const text = 'Decap CMS';
cy.focused()
.getMarkdownEditor()
.type(text)
.setSelection(text)
.clickLinkButton();
cy.confirmMarkdownEditorContent(`<p><a>${text}</a></p>`);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(300);
cy.clickModeToggle();
cy.confirmRawEditorContent(`[${text}](${link})`);
});
});
});

View File

@@ -0,0 +1,736 @@
import '../utils/dismiss-local-backup';
describe('Markdown widget', () => {
describe('list', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.loginAndNewPost();
cy.clearMarkdownEditorContent();
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
// describe('toolbar buttons', () => {
it('creates and focuses empty list', () => {
cy.clickUnorderedListButton().confirmMarkdownEditorContent(`
<ul>
<li>
<p></p>
</li>
</ul>
`);
});
it('removes list', () => {
cy.clickUnorderedListButton().clickUnorderedListButton().confirmMarkdownEditorContent(`
<p></p>
`);
});
it('converts a list item to a paragraph block which is a sibling of the parent list', () => {
cy.clickUnorderedListButton().type('foo').enter().clickUnorderedListButton()
.confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
</li>
</ul>
<p></p>
`);
});
it('converts empty nested list item to empty paragraph block in parent list item', () => {
cy
.clickUnorderedListButton()
.type('foo')
.enter()
.tabkey()
.type('bar')
.enter()
.tabkey()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p>bar</p>
<ul>
<li>
<p></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
`,
)
.clickUnorderedListButton()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p>bar</p>
<p></p>
</li>
</ul>
</li>
</ul>
`,
)
.backspace({ times: 4 })
.clickUnorderedListButton().confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
<p></p>
</li>
</ul>
`);
});
it('moves nested list item content to parent list item when in first block', () => {
cy
.clickUnorderedListButton()
.type('foo')
.enter()
.tabkey()
.type('bar')
.enter()
.tabkey()
.type('baz')
.clickUnorderedListButton()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p>bar</p>
<p>baz</p>
</li>
</ul>
</li>
</ul>
`,
)
.up()
.clickUnorderedListButton()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
<p>bar</p>
<p>baz</p>
</li>
</ul>
`,
)
.up()
.clickUnorderedListButton().confirmMarkdownEditorContent(`
<p>foo</p>
<p>bar</p>
<p>baz</p>
`);
});
it('affects only the current block with collapsed selection', () => {
cy
.focused()
.type('foo')
.enter()
.type('bar')
.enter()
.type('baz')
.up()
.clickUnorderedListButton().confirmMarkdownEditorContent(`
<p>foo</p>
<ul>
<li>
<p>bar</p>
</li>
</ul>
<p>baz</p>
`);
});
it('wrap each bottom-most block in a selection with a list item block', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy
.focused()
.type('foo')
.enter()
.type('bar')
.enter()
.type('baz')
.setSelection('foo', 'baz')
.wait(500)
.clickUnorderedListButton().confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
</li>
<li>
<p>baz</p>
</li>
</ul>
`);
});
it('unwraps list item block from each selected list item and unwraps all of them from the outer list block', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy
.clickUnorderedListButton()
.type('foo')
.enter()
.type('bar')
.enter()
.type('baz')
.setSelection('foo', 'baz')
.wait(500)
.clickUnorderedListButton().confirmMarkdownEditorContent(`
<p>foo</p>
<p>bar</p>
<p>baz</p>
`);
});
it('combines adjacent same-typed lists, not differently typed lists', () => {
cy.focused()
.type('foo')
.enter()
.type('bar')
.enter()
.type('baz')
.up()
.clickUnorderedListButton()
.up()
.clickUnorderedListButton()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
</li>
</ul>
<p>baz</p>
`,
)
.down({ times: 2 })
.focused()
.clickUnorderedListButton()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
</li>
<li>
<p>baz</p>
</li>
</ul>
`,
)
.up()
.enter()
.type('qux')
.tabkey()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
<ul>
<li>
<p>qux</p>
</li>
</ul>
</li>
<li>
<p>baz</p>
</li>
</ul>
`,
)
.up()
.enter()
.type('quux')
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
<ul>
<li>
<p>quux</p>
</li>
<li>
<p>qux</p>
</li>
</ul>
</li>
<li>
<p>baz</p>
</li>
</ul>
`,
)
.clickOrderedListButton()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
<ol>
<li>
<p>quux</p>
</li>
</ol>
<ul>
<li>
<p>qux</p>
</li>
</ul>
</li>
<li>
<p>baz</p>
</li>
</ul>
`,
)
.setSelection({
anchorQuery: 'ul > li > ol p',
anchorOffset: 1,
focusQuery: 'ul > li > ul:last-child p',
focusOffset: 2,
});
});
// while this works on dev environment, it will always fail in cypress - has something to do with text selection
// it('affects only selected list items', () => {
// cy
// .clickUnorderedListButton()
// .type('foo')
// .enter()
// .type('bar')
// .enter()
// .type('baz')
// .setSelection('bar')
// .clickUnorderedListButton()
// .confirmMarkdownEditorContent(
// `
// <ul>
// <li>
// <p>foo</p>
// </li>
// </ul>
// <p>bar</p>
// <ul>
// <li>
// <p>baz</p>
// </li>
// </ul>
// `,
// )
// .clickUnorderedListButton()
// .setSelection('bar', 'baz')
// .clickUnorderedListButton()
// .confirmMarkdownEditorContent(
// `
// <ul>
// <li>
// <p>foo</p>
// </li>
// </ul>
// <p>bar</p>
// <p>baz</p>
// `,
// )
// .clickUnorderedListButton()
// .confirmMarkdownEditorContent(
// `
// <ul>
// <li>
// <p>foo</p>
// </li>
// <li>
// <p>bar</p>
// </li>
// <li>
// <p>baz</p>
// </li>
// </ul>
// `,
// )
// .setSelection('baz')
// .clickUnorderedListButton()
// .confirmMarkdownEditorContent(
// `
// <ul>
// <li>
// <p>foo</p>
// </li>
// <li>
// <p>bar</p>
// </li>
// </ul>
// <p>baz</p>
// `,
// )
// .clickUnorderedListButton()
// .tabkey()
// .setCursorAfter('baz')
// .enter()
// .tabkey()
// .type('qux')
// .confirmMarkdownEditorContent(
// `
// <ul>
// <li>
// <p>foo</p>
// </li>
// <li>
// <p>bar</p>
// <ul>
// <li>
// <p>baz</p>
// <ul>
// <li>
// <p>qux</p>
// </li>
// </ul>
// </li>
// </ul>
// </li>
// </ul>
// `,
// )
// .setSelection('baz')
// .clickOrderedListButton()
// .confirmMarkdownEditorContent(
// `
// <ul>
// <li>
// <p>foo</p>
// </li>
// <li>
// <p>bar</p>
// <ol>
// <li>
// <p>baz</p>
// <ul>
// <li>
// <p>qux</p>
// </li>
// </ul>
// </li>
// </ol>
// </li>
// </ul>
// `,
// )
// .setCursorAfter('qux')
// .enter({ times: 2 })
// .clickUnorderedListButton()
// .confirmMarkdownEditorContent(`
// <ul>
// <li>
// <p>foo</p>
// </li>
// <li>
// <p>bar</p>
// <ol>
// <li>
// <p>baz</p>
// <ul>
// <li>
// <p>qux</p>
// </li>
// </ul>
// </li>
// </ol>
// <ul>
// <li>
// <p></p>
// </li>
// </ul>
// </li>
// </ul>
// `);
// });
// });
// });
// describe('on Enter', () => {
it('removes the list item and list if empty', () => {
cy.clickUnorderedListButton().enter().confirmMarkdownEditorContent(`
<p></p>
`);
});
it('creates a new list item in a non-empty list', () => {
cy
.clickUnorderedListButton()
.type('foo')
.enter()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p></p>
</li>
</ul>
`,
)
.type('bar')
.enter().confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
</li>
<li>
<p></p>
</li>
</ul>
`);
});
it('creates a new default block below a list when hitting Enter twice on an empty list item of the list', () => {
cy.clickUnorderedListButton().type('foo').enter({ times: 2 }).confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
</li>
</ul>
<p></p>
`);
});
// });
// describe('on Backspace', () => {
it('removes the list item and list if empty', () => {
cy.clickUnorderedListButton().backspace().confirmMarkdownEditorContent(`
<p></p>
`);
});
it('removes the list item if list not empty', () => {
cy.clickUnorderedListButton().type('foo').enter().backspace().confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
<p></p>
</li>
</ul>
`);
});
it('does not remove list item if empty with non-default block', () => {
cy.clickUnorderedListButton().clickHeadingOneButton().backspace()
.confirmMarkdownEditorContent(`
<ul>
<li>
<p></p>
</li>
</ul>
`);
});
// });
// describe('on Tab', () => {
it('does nothing in top level list', () => {
cy
.clickUnorderedListButton()
.tabkey()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p></p>
</li>
</ul>
`,
)
.type('foo')
.tabkey().confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
</li>
</ul>
`);
});
it('indents nested list items', () => {
cy
.clickUnorderedListButton()
.type('foo')
.enter()
.type('bar')
.tabkey()
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p>bar</p>
</li>
</ul>
</li>
</ul>
`,
)
.enter()
.tabkey().confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p>bar</p>
<ul>
<li>
<p></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
`);
});
it('only nests up to one level down from the parent list', () => {
cy.clickUnorderedListButton().type('foo').enter().tabkey().confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p></p>
</li>
</ul>
</li>
</ul>
`);
});
it('unindents nested list items with shift', () => {
cy.clickUnorderedListButton().type('foo').enter().tabkey().tabkey({ shift: true })
.confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p></p>
</li>
</ul>
`);
});
it('indents and unindents from one level below parent back to document root', () => {
cy
.clickUnorderedListButton()
.type('foo')
.enter()
.tabkey()
.type('bar')
.enter()
.tabkey()
.type('baz')
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p>bar</p>
<ul>
<li>
<p>baz</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
`,
)
.tabkey({ shift: true })
.confirmMarkdownEditorContent(
`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p>bar</p>
</li>
<li>
<p>baz</p>
</li>
</ul>
</li>
</ul>
`,
)
.tabkey({ shift: true }).confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
<ul>
<li>
<p>bar</p>
</li>
</ul>
</li>
<li>
<p>baz</p>
</li>
</ul>
`);
});
// });
});
});

View File

@@ -0,0 +1,37 @@
import '../utils/dismiss-local-backup';
describe('Markdown widget', () => {
describe('code mark', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.loginAndNewPost();
cy.clearMarkdownEditorContent();
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
describe('toolbar button', () => {
it('can combine code mark with other marks', () => {
cy.clickItalicButton()
.type('foo')
.setSelection('oo')
.clickCodeButton()
.confirmMarkdownEditorContent(`
<p>
<em>f</em>
<code>
<em>oo</em>
</code>
</p>
`);
});
});
});
});

View File

@@ -0,0 +1,360 @@
import '../utils/dismiss-local-backup';
describe('Markdown widget', () => {
describe('quote block', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
beforeEach(() => {
cy.loginAndNewPost();
cy.clearMarkdownEditorContent();
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
// describe('toggle quote', () => {
it('toggles empty quote block on and off in empty editor', () => {
cy.clickQuoteButton()
.confirmMarkdownEditorContent(`
<blockquote>
<p></p>
</blockquote>
`)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<p></p>
`);
});
it('toggles empty quote block on and off for current block', () => {
cy.focused()
.type('foo')
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
</blockquote>
`)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<p>foo</p>
`);
});
it('toggles entire quote block without expanded selection', () => {
cy.clickQuoteButton()
.type('foo')
.enter()
.type('bar')
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<p>foo</p>
<p>bar</p>
`);
});
it('toggles entire quote block with complex content', () => {
cy.clickQuoteButton()
.clickUnorderedListButton()
.clickHeadingOneButton()
.type('foo')
.enter({ times: 2 }) // First Enter creates new list item. Second Enter turns that list item into a default block.
.clickQuoteButton() // Unwrap the quote block.
.confirmMarkdownEditorContent(`
<ul>
<li>
<h1>foo</h1>
</li>
</ul>
<p></p>
`);
});
it('toggles empty quote block on and off for selected blocks', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.focused()
.type('foo')
.enter()
.type('bar')
.setSelection('foo', 'bar')
.wait(500)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
<p>bar</p>
</blockquote>
`)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<p>foo</p>
<p>bar</p>
`)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
<p>bar</p>
</blockquote>
`);
});
it('toggles empty quote block on and off for partially selected blocks', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.focused()
.type('foo')
.enter()
.type('bar')
.setSelection('oo', 'ba')
.wait(500)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
<p>bar</p>
</blockquote>
`)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<p>foo</p>
<p>bar</p>
`)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
<p>bar</p>
</blockquote>
`);
});
it('toggles quote block on and off for multiple selected list items', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.focused()
.clickUnorderedListButton()
.type('foo')
.enter()
.type('bar')
.setSelection('foo', 'bar')
.wait(500)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<blockquote>
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
</li>
</ul>
</blockquote>
`)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
</li>
<li>
<p>bar</p>
</li>
</ul>
`)
.setCursorAfter('bar')
.wait(500)
.enter()
.type('baz')
.setSelection('bar', 'baz')
.wait(500)
.clickQuoteButton()
.confirmMarkdownEditorContent(`
<ul>
<li>
<p>foo</p>
</li>
</ul>
<blockquote>
<ul>
<li>
<p>bar</p>
</li>
<li>
<p>baz</p>
</li>
</ul>
</blockquote>
`)
});
it('creates new quote block if parent is not a quote, can deeply nest', () => {
cy.clickQuoteButton()
.clickUnorderedListButton()
.clickQuoteButton()
.clickUnorderedListButton()
.clickQuoteButton()
.clickUnorderedListButton()
.clickQuoteButton()
.type('foo')
// Content should contains 4 <blockquote> tags and 3 <ul> tags
.confirmMarkdownEditorContent(`
<blockquote>
<ul>
<li>
<blockquote>
<ul>
<li>
<blockquote>
<ul>
<li>
<blockquote>
<p>foo</p>
</blockquote>
</li>
</ul>
</blockquote>
</li>
</ul>
</blockquote>
</li>
</ul>
</blockquote>
`)
/*
* First Enter creates new paragraph within the innermost block quote.
* Second Enter moves that paragraph one level up to become sibling of the previous quote block and direct child of a list item.
* Third Enter to turn that paragraph into a list item and move it one level up.
* Repeat the circle for three more times to reach the second list item of the outermost list block.
* Then Enter again to turn that list item into a paragraph and move it one level up to become sibling of the outermost list and
* direct child of the outermost block quote.
*/
.enter({ times: 10 })
.type('bar')
.confirmMarkdownEditorContent(`
<blockquote>
<ul>
<li>
<blockquote>
<ul>
<li>
<blockquote>
<ul>
<li>
<blockquote>
<p>foo</p>
</blockquote>
</li>
</ul>
</blockquote>
</li>
</ul>
</blockquote>
</li>
</ul>
<p>bar</p>
</blockquote>
`)
/* The GOAL is to delete all the text content inside this deeply nested block quote and turn it into a default paragraph block on top level.
* We need:
* 3 Backspace to delete the word “bar”.
* 1 Backspace to remove the paragraph that contains bar and bring cursor to the end of the unordered list which is direct child of the outermost block quote.
* 3 Backspace to remove the word “foo”.
* 1 Backspace to remove the current block quote that the cursor is on, 1 Backspace to remove the list that wraps the block quote. Repeat this step for three times for a total of 6 Backspace until the cursor is on the outermost block quote.
* 1 Backspace to remove to toggle off the outermost block quote and turn it into a default paragraph.
* Total Backspaces required: 3 + 1 + 3 + ((1 + 1) * 3) + 1 = 14
*/
.backspace({ times: 14 })
});
// });
// describe('backspace inside quote', () => {
it('joins two paragraphs', () => {
cy.clickQuoteButton()
.type('foo')
.enter()
.type('bar')
.setCursorBefore('bar')
.backspace()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foobar</p>
</blockquote>
`);
});
it('joins quote with previous quote', () => {
cy.clickQuoteButton()
.type('foo')
.enter({ times: 2 })
.clickQuoteButton()
.type('bar')
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
</blockquote>
<blockquote>
<p>bar</p>
</blockquote>
`)
.setCursorBefore('bar')
.backspace()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
<p>bar</p>
</blockquote>
`);
});
it('removes first block from quote when focused at first block at start', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.clickQuoteButton()
.type('foo')
.enter()
.type('bar')
.setCursorBefore('foo')
.wait(500)
.backspace()
.confirmMarkdownEditorContent(`
<p>foo</p>
<blockquote>
<p>bar</p>
</blockquote>
`)
});
// });
// describe('enter inside quote', () => {
it('creates new block inside quote', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.clickQuoteButton()
.type('foo')
.enter()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
<p></p>
</blockquote>
`)
.type('bar')
.setCursorAfter('ba')
.wait(500)
.enter()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
<p>ba</p>
<p>r</p>
</blockquote>
`);
});
it('creates new block after quote from empty last block', () => {
cy.clickQuoteButton()
.type('foo')
.enter()
.enter()
.confirmMarkdownEditorContent(`
<blockquote>
<p>foo</p>
</blockquote>
<p></p>
`)
});
// });
});
});

View File

@@ -0,0 +1,27 @@
import fixture from './common/media_library';
import { entry1 } from './common/entries';
import * as specUtils from './common/spec_utils';
const backend = 'bitbucket';
describe('BitBucket Backend Media Library - REST API', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, {}, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
});

View File

@@ -0,0 +1,28 @@
import fixture from './common/media_library';
import { entry1 } from './common/entries';
import * as specUtils from './common/spec_utils';
const backend = 'bitbucket';
const lfs = true;
describe('BitBucket Backend Media Library - Large Media', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { lfs }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
});

View File

@@ -0,0 +1,28 @@
import fixture from './common/media_library';
import { entry1 } from './common/entries';
import * as specUtils from './common/spec_utils';
const backend = 'git-gateway';
const provider = 'github';
describe('Git Gateway (GitHub) Backend Media Library - Large Media', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', provider }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
});

View File

@@ -0,0 +1,28 @@
import fixture from './common/media_library';
import { entry1 } from './common/entries';
import * as specUtils from './common/spec_utils';
const backend = 'git-gateway';
const provider = 'gitlab';
describe('Git Gateway (GitLab) Backend Media Library - Large Media', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', provider }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
});

View File

@@ -0,0 +1,34 @@
import fixture from './common/media_library';
import { entry1 } from './common/entries';
import * as specUtils from './common/spec_utils';
const backend = 'github';
describe('GitHub Backend Media Library - GraphQL API', () => {
const taskResult = { data: {} };
before(() => {
specUtils.before(
taskResult,
{
backend: { use_graphql: true },
publish_mode: 'editorial_workflow',
},
backend,
);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
});

View File

@@ -0,0 +1,34 @@
import fixture from './common/media_library';
import { entry1 } from './common/entries';
import * as specUtils from './common/spec_utils';
const backend = 'github';
describe('GitHub Backend Media Library - REST API', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(
taskResult,
{
backend: { use_graphql: false },
publish_mode: 'editorial_workflow',
},
backend,
);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
});

View File

@@ -0,0 +1,27 @@
import fixture from './common/media_library';
import { entry1 } from './common/entries';
import * as specUtils from './common/spec_utils';
const backend = 'gitlab';
describe('GitLab Backend Media Library - REST API', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow' }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
});

View File

@@ -0,0 +1,29 @@
import fixture from './common/media_library';
import * as specUtils from './common/spec_utils';
import { entry1 } from './common/entries';
const backend = 'proxy';
const mode = 'git';
describe(`Proxy Backend Media Library - '${mode}' mode`, () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', mode }, backend);
Cypress.config('defaultCommandTimeout', 5 * 1000);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
});

View File

@@ -0,0 +1,21 @@
import fixture from './common/media_library';
const entries = [
{
title: 'first title',
body: 'first body',
},
];
describe('Test Backend Media Library', () => {
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
fixture({ entries });
});

View File

@@ -0,0 +1,71 @@
import { login } from '../utils/steps';
const search = (term, collection) => {
cy.get('[class*=SearchInput]').clear({ force: true });
cy.get('[class*=SearchInput]').type(term, { force: true });
cy.get('[class*=SuggestionsContainer]').within(() => {
cy.contains(collection).click();
});
};
const assertSearchHeading = title => {
cy.get('[class*=SearchResultHeading]').should('have.text', title);
};
const assertSearchResult = (text, collection) => {
cy.get('[class*=ListCardLink] h2').contains(collection ?? text)
};
const assertNotInSearch = text => {
cy.get('[class*=ListCardLink] h2').contains(text).should('not.exist');
};
describe('Search Suggestion', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
beforeEach(() => {
login();
});
it('can search in all collections', () => {
search('this', 'All Collections');
assertSearchHeading('Search Results for "this"');
assertSearchResult('This is post # 20', 'Posts');
assertSearchResult('This is a TOML front matter post', 'Posts');
assertSearchResult('This is a JSON front matter post', 'Posts');
assertSearchResult('This is a YAML front matter post', 'Posts');
assertSearchResult('This FAQ item # 5', 'FAQ');
});
it('can search in posts collection', () => {
search('this', 'Posts');
assertSearchHeading('Search Results for "this" in Posts');
assertSearchResult('This is post # 20');
assertSearchResult('This is a TOML front matter post');
assertSearchResult('This is a JSON front matter post');
assertSearchResult('This is a YAML front matter post');
assertNotInSearch('This FAQ item # 5');
});
it('can search in faq collection', () => {
search('this', 'FAQ');
assertSearchHeading('Search Results for "this" in FAQ');
assertSearchResult('This FAQ item # 5');
assertNotInSearch('This is post # 20');
});
});

View File

@@ -0,0 +1,30 @@
import fixture from './common/simple_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'bitbucket';
describe('BitBucket Backend Simple Workflow', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'simple' }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,31 @@
import fixture from './common/simple_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'git-gateway';
const provider = 'github';
describe('Git Gateway (GitHub) Backend Simple Workflow', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'simple', provider }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,31 @@
import fixture from './common/simple_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'git-gateway';
const provider = 'gitlab';
describe('Git Gateway (GitLab) Backend Simple Workflow', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'simple', provider }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,37 @@
import fixture from './common/simple_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'github';
describe('GitHub Backend Simple Workflow - GraphQL API', () => {
const taskResult = { data: {} };
before(() => {
specUtils.before(
taskResult,
{
backend: { use_graphql: true },
publish_mode: 'simple',
},
backend,
);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,37 @@
import fixture from './common/simple_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'github';
describe('GitHub Backend Simple Workflow - REST API', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(
taskResult,
{
backend: { use_graphql: false },
publish_mode: 'simple',
},
backend,
);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,30 @@
import fixture from './common/simple_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'gitlab';
describe('GitLab Backend Simple Workflow', () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'simple' }, backend);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,32 @@
import fixture from './common/simple_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'proxy';
const mode = 'fs';
describe(`Proxy Backend Simple Workflow - '${mode}' mode`, () => {
const taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'simple', mode }, backend);
Cypress.config('defaultCommandTimeout', 5 * 1000);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,35 @@
import fixture from './common/simple_workflow';
import * as specUtils from './common/spec_utils';
import { entry1, entry2, entry3 } from './common/entries';
const backend = 'proxy';
const mode = 'git';
describe(`Proxy Backend Simple Workflow - '${mode}' mode`, () => {
let taskResult = { data: {} };
before(() => {
specUtils.before(taskResult, { publish_mode: 'simple', mode }, backend);
Cypress.config('defaultCommandTimeout', 5 * 1000);
});
after(() => {
specUtils.after(taskResult, backend);
});
beforeEach(() => {
if (Cypress.mocha.getRunner().suite.ctx.currentTest.title === 'can create an entry') {
Cypress.mocha.getRunner().suite.ctx.currentTest.skip();
}
specUtils.beforeEach(taskResult, backend);
});
afterEach(() => {
specUtils.afterEach(taskResult, backend);
});
fixture({
entries: [entry1, entry2, entry3],
getUser: () => taskResult.data.user,
});
});

View File

@@ -0,0 +1,102 @@
import '../utils/dismiss-local-backup';
import {
login,
newPost,
populateEntry,
exitEditor,
createPostAndPublish,
assertPublishedEntry,
editPostAndPublish,
createPostPublishAndCreateNew,
createPostPublishAndDuplicate,
editPostPublishAndCreateNew,
editPostPublishAndDuplicate,
duplicatePostAndPublish,
} from '../utils/steps';
const entry1 = {
title: 'first title',
body: 'first body',
};
const entry2 = {
title: 'second title',
body: 'second body',
};
const backend = 'test';
describe('Test Backend Simple Workflow', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend, options: { publish_mode: 'simple' } });
});
after(() => {
cy.task('teardownBackend', { backend });
});
it('successfully loads', () => {
login();
});
it('can create a new entry', () => {
login();
newPost();
populateEntry(entry1, () => {});
// new entry should show 'Unsaved changes'
cy.contains('div', 'Unsaved Changes');
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
exitEditor();
});
it('can publish a new entry', () => {
login();
createPostAndPublish(entry1);
assertPublishedEntry(entry1);
});
it('can publish a new entry and create new', () => {
login();
createPostPublishAndCreateNew(entry1);
assertPublishedEntry(entry1);
});
it('can publish a new entry and duplicate', () => {
login();
createPostPublishAndDuplicate(entry1);
assertPublishedEntry(entry1);
});
it('can edit an existing entry and publish', () => {
login();
createPostAndPublish(entry1);
assertPublishedEntry(entry1);
editPostAndPublish(entry1, entry2);
});
it('can edit an existing entry, publish and create new', () => {
login();
createPostAndPublish(entry1);
assertPublishedEntry(entry1);
editPostPublishAndCreateNew(entry1, entry2);
});
it('can edit an existing entry, publish and duplicate', () => {
login();
createPostAndPublish(entry1);
assertPublishedEntry(entry1);
editPostPublishAndDuplicate(entry1, entry2);
});
it('can duplicate an existing entry', () => {
login();
createPostAndPublish(entry1);
assertPublishedEntry(entry1);
duplicatePostAndPublish(entry1);
});
});

View File

@@ -0,0 +1,102 @@
import { login } from '../utils/steps';
const filter = term => {
cy.contains('span', 'Filter by').click();
cy.contains(term).click();
cy.contains('Contents').click();
};
const assertEntriesCount = count => {
cy.get('[class*=ListCardLink]').should('have.length', count);
};
const assertInEntries = text => {
cy.get('[class*=ListCardLink] h2').contains(text);
};
const assertNotInEntries = text => {
cy.get('[class*=ListCardLink] h2').contains(text).should('not.exist');
};
describe('View Filter', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
beforeEach(() => {
login();
});
it('can apply string filter', () => {
// enable filter
filter('Posts With Index');
assertEntriesCount(20);
for (let i = 1; i <= 20; i++) {
assertInEntries(`This is post # ${i} --`);
}
assertNotInEntries('This is a YAML front matter post');
assertNotInEntries('This is a JSON front matter post');
assertNotInEntries('This is a TOML front matter post');
// disable filter
filter('Posts With Index');
assertEntriesCount(23);
for (let i = 1; i <= 20; i++) {
assertInEntries(`This is post # ${i} --`);
}
assertInEntries('This is a YAML front matter post');
assertInEntries('This is a JSON front matter post');
assertInEntries('This is a TOML front matter post');
});
it('can apply boolean filter', () => {
// enable filter
filter('Drafts');
assertEntriesCount(10);
for (let i = 1; i <= 20; i++) {
const draft = i % 2 === 0;
if (draft) {
assertInEntries(`This is post # ${i} --`);
} else {
assertNotInEntries(`This is post # ${i} --`);
}
}
assertNotInEntries('This is a YAML front matter post');
assertNotInEntries('This is a JSON front matter post');
assertNotInEntries('This is a TOML front matter post');
// disable filter
filter('Drafts');
assertEntriesCount(23);
for (let i = 1; i <= 20; i++) {
assertInEntries(`This is post # ${i} --`);
}
assertInEntries('This is a YAML front matter post');
assertInEntries('This is a JSON front matter post');
assertInEntries('This is a TOML front matter post');
});
it('can apply multiple filters', () => {
// enable filter
filter('Posts Without Index');
assertEntriesCount(3);
assertInEntries('This is a YAML front matter post');
assertInEntries('This is a JSON front matter post');
assertInEntries('This is a TOML front matter post');
filter('Drafts');
assertEntriesCount(0);
cy.contains('div', 'No Entries');
});
});

View File

@@ -0,0 +1,70 @@
import { login } from '../utils/steps';
const group = term => {
cy.contains('span', 'Group by').click();
cy.contains(term).click();
cy.contains('Contents').click();
};
const assertGroupsCount = count => {
cy.get('[class*=GroupContainer]').should('have.length', count);
};
const assertEachGroupCount = (id, count) => {
cy.get(`[id='${id}']`).within(() => {
assertEntriesCount(count);
});
};
const assertEntriesCount = count => {
cy.get('[class*=ListCardLink]').should('have.length', count);
};
const assertInEntries = text => {
cy.get('[class*=ListCardLink] h2').contains('h2', text);
};
describe('View Group', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
after(() => {
cy.task('teardownBackend', { backend: 'test' });
});
beforeEach(() => {
login();
});
it('can apply string group', () => {
// enable group
group('Year');
assertGroupsCount(2);
const year = new Date().getFullYear();
assertEachGroupCount(`Year${year}`, 20);
assertEachGroupCount('Year2015', 3);
//disable group
group('Year');
assertEntriesCount(23);
for (let i = 1; i <= 20; i++) {
assertInEntries(`This is post # ${i} --`);
}
assertInEntries('This is a YAML front matter post');
assertInEntries('This is a JSON front matter post');
assertInEntries('This is a TOML front matter post');
//enable group
group('Drafts');
assertEntriesCount(23);
assertGroupsCount(3);
assertEachGroupCount('Draftstrue', 10);
assertEachGroupCount('Draftsfalse', 10);
assertEachGroupCount('missing_value', 3);
});
});