feat(backend-github): GitHub GraphQL API support ()

* add GitHub GraphQL api initial support

* support mutiple backends for e2e tests - initial commit

* add github backend e2e test (currently skipped), fix bugs per tests

* refactor e2e tests, add fork workflow tests, support fork workflow in GraphQL api

* remove log message that might contain authentication token

* return empty error when commit is not found when using GraphQL (align with base class)

* disable github backend tests

* fix bugs introduced after rebase of GraphQL and OpenAuthoring features

* test: update tests per openAuthoring changes, split tests into multiple files

* fix: pass in headers for pagination requests, avoid async iterator as it requires a polyfill on old browsers

* test(e2e): disable github backend tests
This commit is contained in:
Erez Rokah
2019-09-03 21:56:20 +03:00
committed by Shawn Erquhart
parent 083a336ba4
commit ece136c92e
34 changed files with 3103 additions and 529 deletions

@ -1,349 +0,0 @@
import '../utils/dismiss-local-backup';
describe('Editorial Workflow', () => {
const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' };
const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
const entry1 = { title: 'first title', body: 'first body' };
const entry2 = { title: 'second title', body: 'second body' };
const entry3 = { title: 'third title', body: 'third body' };
const setting1 = { limit: 10, author: 'John Doe' };
const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' };
const notifications = {
saved: 'Entry saved',
published: 'Entry published',
updated: 'Entry status updated',
deletedUnpublished: 'Unpublished changes deleted',
error: {
missingField: "Oops, you've missed a required field. Please complete before saving.",
},
validation: {
range: {
fieldLabel: 'Number of posts on frontpage',
message: 'Number of posts on frontpage must be between 1 and 10.',
},
},
};
describe('Test Backend', () => {
function login() {
cy.viewport(1200, 1200);
cy.visit('/');
cy.contains('button', 'Login').click();
}
function createPost({ title, body }) {
cy.contains('a', 'New Post').click();
cy.get('input')
.first()
.type(title);
cy.get('[data-slate-editor]')
.click()
.type(body);
cy.get('input')
.first()
.click();
cy.contains('button', 'Save').click();
assertNotification(notifications.saved);
}
function validateObjectFields({ limit, author }) {
cy.get('a[href^="#/collections/settings"]').click();
cy.get('a[href^="#/collections/settings/entries/general"]').click();
cy.get('input[type=number]').type(limit);
cy.contains('button', 'Save').click();
assertNotification(notifications.error.missingField);
cy.contains('label', 'Default Author').click();
cy.focused().type(author);
cy.contains('button', 'Save').click();
assertNotification(notifications.saved);
}
function validateNestedObjectFields({ limit, author }) {
cy.get('a[href^="#/collections/settings"]').click();
cy.get('a[href^="#/collections/settings/entries/general"]').click();
cy.contains('label', 'Default Author').click();
cy.focused().type(author);
cy.contains('button', 'Save').click();
assertNotification(notifications.error.missingField);
cy.get('input[type=number]').type(limit + 1);
cy.contains('button', 'Save').click();
assertFieldValidationError(notifications.validation.range);
cy.get('input[type=number]')
.clear()
.type(-1);
cy.contains('button', 'Save').click();
assertFieldValidationError(notifications.validation.range);
cy.get('input[type=number]')
.clear()
.type(limit);
cy.contains('button', 'Save').click();
assertNotification(notifications.saved);
}
function validateListFields({ name, description }) {
cy.get('a[href^="#/collections/settings"]').click();
cy.get('a[href^="#/collections/settings/entries/authors"]').click();
cy.contains('button', 'Add').click();
cy.contains('button', 'Save').click();
assertNotification(notifications.error.missingField);
cy.get('input')
.eq(2)
.type(name);
cy.get('[data-slate-editor]')
.eq(2)
.type(description);
cy.contains('button', 'Save').click();
}
function exitEditor() {
cy.contains('a[href^="#/collections/"]', 'Writing in').click();
}
function deleteEntryInEditor() {
cy.contains('button', 'Delete').click();
assertNotification(notifications.deletedUnpublished);
}
function assertEntryDeleted(entry) {
if (Array.isArray(entry)) {
const titles = entry.map(e => e.title);
cy.get('a h2').each((el, idx) => {
expect(titles).not.to.include(el.text());
});
} else {
cy.get('a h2').each((el, idx) => {
expect(entry.title).not.to.equal(el.text());
});
}
}
function createAndDeletePost(entry) {
createPost(entry);
deleteEntryInEditor();
}
function createPostAndExit(entry) {
createPost(entry);
exitEditor();
}
function createPublishedPost(entry) {
createPost(entry);
updateWorkflowStatusInEditor(editorStatus.ready);
publishInEditor();
}
function createPublishedPostAndExit(entry) {
createPublishedPost(entry);
exitEditor();
}
function validateObjectFieldsAndExit(setting) {
validateObjectFields(setting);
exitEditor();
}
function validateNestedObjectFieldsAndExit(setting) {
validateNestedObjectFields(setting);
exitEditor();
}
function validateListFieldsAndExit(setting) {
validateListFields(setting);
exitEditor();
}
function goToWorkflow() {
cy.contains('a', 'Workflow').click();
}
function goToCollections() {
cy.contains('a', 'Content').click();
}
function updateWorkflowStatus({ title }, fromColumnHeading, toColumnHeading) {
cy.contains('h2', fromColumnHeading)
.parent()
.contains('a', title)
.trigger('dragstart', {
dataTransfer: {},
force: true,
});
cy.contains('h2', toColumnHeading)
.parent()
.trigger('drop', {
dataTransfer: {},
force: true,
});
assertNotification(notifications.updated);
}
function assertWorkflowStatus({ title }, status) {
cy.contains('h2', status)
.parent()
.contains('a', title);
}
function updateWorkflowStatusInEditor(newStatus) {
cy.contains('[role="button"]', 'Set status').as('setStatusButton');
cy.get('@setStatusButton')
.parent()
.within(() => {
cy.get('@setStatusButton').click();
cy.contains('[role="menuitem"] span', newStatus).click();
});
assertNotification(notifications.updated);
}
function assertWorkflowStatusInEditor(status) {
cy.contains('[role="button"]', 'Set status').as('setStatusButton');
cy.get('@setStatusButton')
.parent()
.within(() => {
cy.get('@setStatusButton').click();
cy.contains('[role="menuitem"] span', status)
.parent()
.within(() => {
cy.get('svg');
});
cy.get('@setStatusButton').click();
});
}
function publishWorkflowEntry({ title }) {
cy.contains('h2', workflowStatus.ready)
.parent()
.within(() => {
cy.contains('a', title)
.parent()
.within(() => {
cy.contains('button', 'Publish new entry').click({ force: true });
});
});
assertNotification(notifications.published);
}
function assertPublishedEntry(entry) {
if (Array.isArray(entry)) {
const entries = entry.reverse();
cy.get('a h2').then(els => {
cy.wrap(els.slice(0, entries.length)).each((el, idx) => {
cy.wrap(el).contains(entries[idx].title);
});
});
} else {
cy.get('a h2')
.first()
.contains(entry.title);
}
}
function assertNotification(message) {
if (Array.isArray(message)) {
const messages = message.reverse();
cy.get('.notif__container div')
.should('have.length.of', messages.length)
.each((el, idx) => {
cy.wrap(el)
.contains(messages[idx])
.invoke('hide');
});
} else {
cy.get('.notif__container').within(() => {
cy.contains(message).invoke('hide');
});
}
}
function assertFieldValidationError({ message, fieldLabel }) {
cy.contains('label', fieldLabel)
.siblings('ul[class*=ControlErrorsList]')
.contains(message);
}
function assertOnCollectionsPage() {
cy.url().should('contain', '/#/collections/posts');
cy.contains('h2', 'Collections');
}
it('successfully loads', () => {
login();
});
it('can create an entry', () => {
login();
createPostAndExit(entry1);
});
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 publish an editorial workflow entry', () => {
login();
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry1);
});
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);
});
});
});

@ -0,0 +1,5 @@
import fixture from './github/editorial_workflow';
describe.skip('Github Backend Editorial Workflow - GraphQL API', () => {
fixture({ use_graphql: true });
});

@ -0,0 +1,5 @@
import fixture from './github/open_authoring';
describe.skip('Github Backend Editorial Workflow - GraphQL API - Open Authoring', () => {
fixture({ use_graphql: true });
});

@ -0,0 +1,5 @@
import fixture from './github/editorial_workflow';
describe.skip('Github Backend Editorial Workflow - REST API', () => {
fixture({ use_graphql: false });
});

@ -0,0 +1,5 @@
import fixture from './github/open_authoring';
describe.skip('Github Backend Editorial Workflow - REST API - Open Authoring', () => {
fixture({ use_graphql: false });
});

@ -0,0 +1,126 @@
import '../utils/dismiss-local-backup';
import {
login,
createPost,
createPostAndExit,
exitEditor,
goToWorkflow,
goToCollections,
updateWorkflowStatus,
publishWorkflowEntry,
assertWorkflowStatusInEditor,
assertPublishedEntry,
deleteEntryInEditor,
assertOnCollectionsPage,
assertEntryDeleted,
assertWorkflowStatus,
updateWorkflowStatusInEditor,
validateObjectFieldsAndExit,
validateNestedObjectFieldsAndExit,
validateListFieldsAndExit,
} from '../utils/steps';
import { setting1, setting2, workflowStatus, editorStatus } 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('restoreDefaults');
});
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend: 'test' });
});
it('successfully loads', () => {
login();
});
it('can create an entry', () => {
login();
createPostAndExit(entry1);
});
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 publish an editorial workflow entry', () => {
login();
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry1);
});
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);
});
});

@ -0,0 +1,117 @@
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';
import { entry1, entry2, entry3 } from './entries';
export default function({ use_graphql }) {
let taskResult = { data: {} };
const backend = 'github';
before(() => {
Cypress.config('taskTimeout', 1200000);
Cypress.config('defaultCommandTimeout', 60000);
cy.task('setupBackend', { backend }).then(data => {
taskResult.data = data;
});
cy.task('updateBackendOptions', { backend, options: { use_graphql, open_authoring: false } });
});
after(() => {
cy.task('teardownBackend', { backend, ...taskResult.data });
cy.task('restoreDefaults');
});
afterEach(() => {
cy.task('teardownBackendTest', { backend, ...taskResult.data });
});
it('successfully loads', () => {
login(taskResult.data.user);
});
it('can create an entry', () => {
login(taskResult.data.user);
createPostAndExit(entry1);
});
it('can update an entry', () => {
login(taskResult.data.user);
createPostAndExit(entry1);
updateExistingPostAndExit(entry1, entry2);
});
it('can publish an editorial workflow entry', () => {
login(taskResult.data.user);
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry1);
});
it('can change workflow status', () => {
login(taskResult.data.user);
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(taskResult.data.user);
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(taskResult.data.user);
createPost(entry1);
deleteEntryInEditor();
assertOnCollectionsPage();
assertEntryDeleted(entry1);
});
it('can update workflow status from within the editor', () => {
login(taskResult.data.user);
createPost(entry1);
assertWorkflowStatusInEditor(editorStatus.draft);
updateWorkflowStatusInEditor(editorStatus.review);
assertWorkflowStatusInEditor(editorStatus.review);
updateWorkflowStatusInEditor(editorStatus.ready);
assertWorkflowStatusInEditor(editorStatus.ready);
exitEditor();
goToWorkflow();
assertWorkflowStatus(entry1, workflowStatus.ready);
});
}

@ -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',
};

@ -0,0 +1,99 @@
import '../../utils/dismiss-local-backup';
import {
login,
createPostAndExit,
updateExistingPostAndExit,
goToWorkflow,
deleteWorkflowEntry,
updateWorkflowStatus,
publishWorkflowEntry,
} from '../../utils/steps';
import { workflowStatus } from '../../utils/constants';
import { entry1, entry2 } from './entries';
export default function({ use_graphql }) {
let taskResult = { data: {} };
const backend = 'github';
before(() => {
Cypress.config('taskTimeout', 1200000);
Cypress.config('defaultCommandTimeout', 60000);
cy.task('setupBackend', { backend }).then(data => {
taskResult.data = data;
});
cy.task('updateBackendOptions', { backend, options: { use_graphql, open_authoring: true } });
});
after(() => {
cy.task('teardownBackend', { backend, ...taskResult.data });
cy.task('restoreDefaults');
});
afterEach(() => {
cy.task('teardownBackendTest', { backend, ...taskResult.data });
});
it('successfully loads', () => {
login(taskResult.data.user);
});
it('can create an entry', () => {
login(taskResult.data.user);
createPostAndExit(entry1);
});
it('can update an entry', () => {
login(taskResult.data.user);
createPostAndExit(entry1);
updateExistingPostAndExit(entry1, entry2);
});
it('can publish an editorial workflow entry', () => {
login(taskResult.data.user);
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entry1);
});
it('successfully forks repository and loads', () => {
login(taskResult.data.forkUser);
});
it('can create an entry on fork', () => {
login(taskResult.data.forkUser);
createPostAndExit(entry1);
});
it('can update a draft entry on fork', () => {
login(taskResult.data.user);
createPostAndExit(entry1);
updateExistingPostAndExit(entry1, entry2);
});
it('can change entry status from fork', () => {
login(taskResult.data.forkUser);
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
});
it('can delete review entry from fork', () => {
login(taskResult.data.forkUser);
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
deleteWorkflowEntry(entry1);
});
it('can return entry to draft and delete it', () => {
login(taskResult.data.forkUser);
createPostAndExit(entry1);
goToWorkflow();
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
deleteWorkflowEntry(entry1);
});
}