feat: v4.0.0 (#1016)

Co-authored-by: Denys Konovalov <kontakt@denyskon.de>
Co-authored-by: Mathieu COSYNS <64072917+Mathieu-COSYNS@users.noreply.github.com>
This commit is contained in:
Daniel Lautzenheiser
2024-01-03 15:14:09 -05:00
committed by GitHub
parent 682576ffc4
commit 799c7e6936
732 changed files with 48477 additions and 10886 deletions

View File

@ -0,0 +1,92 @@
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({ user: getUser() });
});
it('can create an entry', () => {
login({ user: getUser() });
createPostAndExit(entries[0]);
});
it('can update an entry', () => {
login({ user: getUser() });
createPostAndExit(entries[0]);
updateExistingPostAndExit(entries[0], entries[1]);
});
it('can publish an editorial workflow entry', () => {
login({ user: getUser() });
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entries[0]);
});
it('can change workflow status', () => {
login({ user: 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({ user: 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({ user: getUser() });
createPost(entries[0]);
deleteEntryInEditor();
assertOnCollectionsPage();
assertEntryDeleted(entries[0]);
});
it('can update workflow status from within the editor', () => {
login({ user: 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,50 @@
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({ user: 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,121 @@
import type { Post } from '@staticcms/cypress/interface';
export const entry1: Post = {
Title: 'first title',
Body: 'first body',
Description: 'first description',
Category: 'first category',
Tags: 'tag1',
};
export const entry2: Post = {
Title: 'second title',
Body: 'second body',
Description: 'second description',
Category: 'second category',
Tags: 'tag2',
};
export const entry3: Post = {
Title: 'third title',
Body: 'third body',
Description: 'third description',
Category: 'third category',
Tags: 'tag3',
};
export const entry4: Post = {
Title: 'fourth title',
Body: 'fourth body',
Description: 'fourth description',
Category: 'fourth category',
Tags: 'tag4',
};
export const entry5: Post = {
Title: 'fifth title',
Body: 'fifth body',
Description: 'fifth description',
Category: 'fifth category',
Tags: 'tag5',
};
export const entry6: Post = {
Title: 'sixth title',
Body: 'sixth body',
Description: 'sixth description',
Category: 'sixth category',
Tags: 'tag6',
};
export const entry7: Post = {
Title: 'seventh title',
Body: 'seventh body',
Description: 'seventh description',
Category: 'seventh category',
Tags: 'tag7',
};
export const entry8: Post = {
Title: 'eighth title',
Body: 'eighth body',
Description: 'eighth description',
Category: 'eighth category',
Tags: 'tag8',
};
export const entry9: Post = {
Title: 'nineth title',
Body: 'nineth body',
Description: 'nineth description',
Category: 'nineth category',
Tags: 'tag9',
};
export const entry10: Post = {
Title: 'tenth title',
Body: 'tenth body',
Description: 'tenth description',
Category: 'tenth category',
Tags: 'tag10',
};
export const entry11: Post = {
Title: 'eleventh title',
Body: 'eleventh body',
Description: 'eleventh description',
Category: 'eleventh category',
Tags: 'tag11',
};
export const entry12: Post = {
Title: 'twelveth title',
Body: 'twelveth body',
Description: 'twelveth description',
Category: 'twelveth category',
Tags: 'tag12',
};
export const entry13: Post = {
Title: 'thirteenth title',
Body: 'thirteenth body',
Description: 'thirteenth description',
Category: 'thirteenth category',
Tags: 'tag13',
};
export const entry14: Post = {
Title: 'fourteenth title',
Body: 'fourteenth body',
Description: 'fourteenth description',
Category: 'fourteenth category',
Tags: 'tag14',
};
export const entry15: Post = {
Title: 'fifteenth title',
Body: 'fifteenth body',
Description: 'fifteenth description',
Category: 'fifteenth category',
Tags: 'tag15',
};

View File

@ -0,0 +1,55 @@
import { newPost, populateEntry, publishEntry, flushClockAndSave } from '../../utils/steps';
const enterTranslation = str => {
cy.get('[data-testid="field-Title"]').first().clear({ force: true });
cy.get('[data-testid="field-Title"]').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('[data-testid="field-Title"]').should('have.value', 'de');
cy.contains('span', 'Writing in DE').click();
cy.contains('span', 'fr').click();
cy.get('[data-testid="field-Title"]').should('have.value', 'fr');
});
};
export const createEntryTranslateAndPublish = entry => {
createAndTranslate(entry);
publishEntry();
};
export const createEntryTranslateAndSave = entry => {
createAndTranslate(entry);
flushClockAndSave();
};

View File

@ -0,0 +1,53 @@
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({ user: 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({ user: getUser() });
createEntryTranslateAndSave(entry);
assertUnpublishedEntryInEditor();
updateWorkflowStatusInEditor(editorStatus.ready);
publishEntryInEditor(publishTypes.publishNow);
exitEditor();
goToEntry(entry);
assertTranslation();
assertPublishedEntryInEditor();
updateTranslation();
assertUnpublishedChangesInEditor();
});
});
}

View File

@ -0,0 +1,162 @@
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.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]);
exitEditor();
goToMediaLibrary();
assertNoImagesInLibrary();
matchImageSnapshot();
});
it('should show published entry image in global media library', () => {
publishPostWithImage(entries[0]);
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,75 @@
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({ user: getUser() });
});
it('can create an entry', () => {
login({ user: getUser() });
createPostAndExit(entries[0]);
});
it('can update an entry', () => {
login({ user: getUser() });
createPostAndExit(entries[0]);
updateExistingPostAndExit(entries[0], entries[1]);
});
it('can publish an editorial workflow entry', () => {
login({ user: getUser() });
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
publishWorkflowEntry(entries[0]);
});
it('successfully forks repository and loads', () => {
login({ user: getForkUser() });
});
it('can create an entry on fork', () => {
login({ user: getForkUser() });
createPostAndExit(entries[0]);
});
it('can update a draft entry on fork', () => {
login({ user: getForkUser() });
createPostAndExit(entries[0]);
updateExistingPostAndExit(entries[0], entries[1]);
});
it('can change entry status from fork', () => {
login({ user: getForkUser() });
createPostAndExit(entries[0]);
goToWorkflow();
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
});
it('can delete review entry from fork', () => {
login({ user: 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({ user: 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,85 @@
import {
assertPublishedEntry,
createPostAndPublish,
createPostPublishAndCreateNew,
createPostPublishAndDuplicate,
duplicatePostAndPublish,
editPostAndPublish,
editPostPublishAndCreateNew,
editPostPublishAndDuplicate,
login,
} from '../../utils/steps';
import {
entry1,
entry10,
entry2,
entry3,
entry4,
entry5,
entry6,
entry7,
entry8,
entry9,
} from './entries';
import type { User } from '@staticcms/cypress/interface';
export interface SimpleWorkflowProps {
getUser?: () => User | undefined;
}
export default function ({ getUser }: SimpleWorkflowProps = {}) {
it('successfully loads', () => {
login({ user: getUser?.() });
});
it('can create an entry', () => {
login({ user: getUser?.() });
createPostAndPublish(entry1);
assertPublishedEntry(entry1);
});
it('can publish a new entry and create new', () => {
login();
createPostPublishAndCreateNew(entry2);
assertPublishedEntry(entry2);
});
it('can publish a new entry and duplicate', () => {
login();
createPostPublishAndDuplicate(entry3);
assertPublishedEntry(entry3);
});
it('can edit an existing entry and publish', () => {
login();
createPostAndPublish(entry4);
assertPublishedEntry(entry4);
editPostAndPublish(entry4, entry5);
});
it('can edit an existing entry, publish and create new', () => {
login();
createPostAndPublish(entry6);
assertPublishedEntry(entry6);
editPostPublishAndCreateNew(entry6, entry7);
});
it('can edit an existing entry, publish and duplicate', () => {
login();
createPostAndPublish(entry8);
assertPublishedEntry(entry8);
editPostPublishAndDuplicate(entry8, entry9);
});
it('can duplicate an existing entry', () => {
login();
createPostAndPublish(entry10);
assertPublishedEntry(entry10);
duplicatePostAndPublish(entry10);
});
}

View File

@ -0,0 +1,47 @@
import type { Config } from '@staticcms/core/interface';
import type { TaskResult } from 'cypress/interface';
export const before = (taskResult: TaskResult, options: Partial<Config>, backend: string) => {
Cypress.config('taskTimeout', 7 * 60 * 1000);
cy.task('setupBackend', { backend, options }).then(data => {
taskResult.data = data;
});
};
export const after = (backend: string) => {
cy.task('teardownBackend', {
backend,
});
};
export const beforeEach = (backend: string) => {
cy.task('setupBackendTest', {
backend,
testName: Cypress.currentTest.title,
});
};
export const afterEach = (backend: string) => {
cy.task('teardownBackendTest', {
backend,
testName: Cypress.currentTest.title,
});
const {
suite: {
ctx: {
currentTest: { state, _retries: retries, _currentRetry: currentRetry },
},
},
} = (Cypress as any).mocha.getRunner();
if (state === 'failed' && retries === currentRetry) {
(Cypress as any).runner.stop();
}
};
export const seedRepo = (backend: string) => {
cy.task('seedRepo', {
backend,
});
};