Skip to content

Commit

Permalink
feat(cm): add content history under future flag (#19315)
Browse files Browse the repository at this point in the history
feat(cm): add content history
  • Loading branch information
remidej committed Mar 5, 2024
2 parents 6679b9c + a47d2a8 commit d28dfbf
Show file tree
Hide file tree
Showing 71 changed files with 3,649 additions and 28 deletions.
1 change: 1 addition & 0 deletions .github/actions/run-e2e-tests/action.yml
Expand Up @@ -14,5 +14,6 @@ runs:
env:
RUN_EE: ${{ inputs.runEE }}
JEST_OPTIONS: ${{ inputs.jestOptions }}
STRAPI_FEATURES_FUTURE_CONTENT_HISTORY: ${{ inputs.enableFutureFeatures }}
STRAPI_FEATURES_FUTURE_RELEASES_SCHEDULING: ${{ inputs.enableFutureFeatures }}
shell: bash
8 changes: 8 additions & 0 deletions docs/docs/guides/e2e/00-setup.md
Expand Up @@ -75,6 +75,14 @@ The test-app you create uses a [template](https://docs.strapi.io/developer-docs/

If you add anything to the template, be sure to add this information to [the docs](/testing/e2e/app-template).

## Running tests with environment variables

To set specific environment variables for your tests, a `.env` file can be created in the root of the e2e folder. This is useful if you need to run tests with a Strapi license or set future flags.

## Running tests with future flags

If you are writing tests for an unstable future feature you will need to add `app-template/config/features.js`. Currently the app template generation does not take the config folder into consideration. However, the run-e2e-tests script will apply the features config to the generated app. See the documentation for [features.js](https://docs.strapi.io/dev-docs/configurations/features#enabling-a-future-flag)

## What is Playwright?

Playwright enables reliable end-to-end testing for modern web apps. It's cross browser, cross platform and cross language. At Strapi we use it for Javascript automated testing.
Expand Down
5 changes: 5 additions & 0 deletions e2e/.env.example
@@ -0,0 +1,5 @@
# Specify any environment variables you would like to use for your tests

# Examples:
STRAPI_LICENSE=your_license_key
STRAPI_FEATURES_FUTURE_CONTENT_HISTORY=true
1 change: 1 addition & 0 deletions e2e/app-template/config/features.js
@@ -1,5 +1,6 @@
module.exports = ({ env }) => ({
future: {
history: env.bool('STRAPI_FEATURES_FUTURE_CONTENT_HISTORY', false),
contentReleasesScheduling: env.bool('STRAPI_FEATURES_FUTURE_RELEASES_SCHEDULING', false),
},
});
Expand Up @@ -8,7 +8,11 @@
"description": ""
},
"options": {},
"pluginOptions": {},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string"
Expand Down
1 change: 1 addition & 0 deletions e2e/constants.js
Expand Up @@ -15,6 +15,7 @@ const ALLOWED_CONTENT_TYPES = [
'api::upcoming-match.upcoming-match',
'api::unique.unique',
'plugin::i18n.locale',
'plugin::content-manager.history-version',
'plugin::content-releases.release',
'plugin::content-releases.release-action',
/**
Expand Down
Binary file modified e2e/data/with-admin.tar
Binary file not shown.
217 changes: 217 additions & 0 deletions e2e/tests/content-manager/history.spec.ts
@@ -0,0 +1,217 @@
import { test, expect } from '@playwright/test';
import { login } from '../../utils/login';
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
import { describeOnCondition } from '../../utils/shared';

const hasFutureFlag = process.env.STRAPI_FEATURES_FUTURE_CONTENT_HISTORY === 'true';

describeOnCondition(hasFutureFlag)('History', () => {
test.describe('Collection Type', () => {
test.beforeEach(async ({ page }) => {
await resetDatabaseAndImportDataFromPath('./e2e/data/with-admin.tar');
await page.goto('/admin');
await login({ page });
});

test('A user should be able create, edit, or publish/unpublish an entry, navigate to the history page, and select versions to view from a list', async ({
page,
}) => {
const CREATE_URL =
/\/admin\/content-manager\/collection-types\/api::article.article\/create(\?.*)?/;
const HISTORY_URL =
/\/admin\/content-manager\/collection-types\/api::article.article\/[^/]+\/history(\?.*)?/;
// Navigate to the content-manager - collection type - article
await page.getByRole('link', { name: 'Content Manager' }).click();
await page.getByRole('combobox', { name: 'Select a locale' }).click();
await page.getByRole('option', { name: 'French (fr)' }).click();
await page.getByRole('link', { name: /Create new entry/, exact: true }).click();
await page.waitForURL(CREATE_URL);

/**
* Create
*/
const titleInput = await page.getByRole('textbox', { name: 'title' });
// Create a french version
const frenchTitle = "N'importe quoi";
await titleInput.fill(frenchTitle);
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('link', { name: 'History' }).click();
await page.waitForURL(HISTORY_URL);
await expect(titleInput).toHaveValue(frenchTitle);

// Go back to the CM to create a new english entry
await page.goto('/admin');
await page.getByRole('link', { name: 'Content Manager' }).click();
await page.getByRole('link', { name: /Create new entry/, exact: true }).click();
await page.waitForURL(CREATE_URL);

// Create an english version
const englishTitle = 'Being from Kansas is a pity';
await titleInput.fill(englishTitle);
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('link', { name: 'History' }).click();
await page.waitForURL(HISTORY_URL);
const versionCards = await page.getByRole('listitem', { name: 'Version card' });
await expect(versionCards).toHaveCount(1);
// Assert the id was added after page load
const idRegex = /id=\d+/;
await expect(idRegex.test(page.url())).toBe(true);
// Assert the most recent version is the current version
const currentVersion = versionCards.nth(0);
await expect(currentVersion.getByText('(current)')).toBeVisible();
await expect(currentVersion.getByText('Draft')).toBeVisible();
await expect(titleInput).toBeDisabled();
await expect(titleInput).toHaveValue(englishTitle);
// Assert only the english versions are available
await expect(page.getByText(frenchTitle)).not.toBeVisible();

// Go back to the entry
await page.getByRole('link', { name: 'Back' }).click();

/**
* Update
*/
await titleInput.fill('Being from Kansas City');
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('link', { name: 'History' }).click();
await expect(versionCards).toHaveCount(2);
// Assert the most recent version is the current version
versionCards.first().click();
await expect(titleInput).toHaveValue('Being from Kansas City');
// Assert the previous version in the list is the expected version
const previousVersion = versionCards.nth(1);
previousVersion.click();
await expect(titleInput).toHaveValue('Being from Kansas is a pity');
await expect(previousVersion.getByText('(current)')).not.toBeVisible();

// Go back to the entry
// await page.getByRole('link', { name: 'Back' }).click();

/**
* Publish
*
* TODO: Fix publish
* The publish action in the middleware used to create history versions has a different shape than the other actions.
* This leaves us with null for status and relatedDocumentId in the history version.
*
*/
// await page.getByRole('button', { name: 'Publish' }).click();
// await page.getByRole('link', { name: 'History' }).click();
// await page.waitForURL(
// '**/content-manager/collection-types/api::article.article/*/history?plugins\\[i18n\\]\\[locale\\]=en'
// );
// await expect(versionCards).toHaveCount(3);
// // Assert the current version is the most recent published version
// await expect(titleInput).toHaveValue('Being from Kansas City');
// await expect(currentVersion.getByText('Published')).toBeVisible();
// // Assert the previous version in the list is the expected version
// await expect(versionCards.nth(1).getByText('Draft')).toBeVisible();
// versionCards.nth(1).click();
// await expect(titleInput).toHaveValue('Being from Kansas City');
});
});

test.describe('Single Type', () => {
test.beforeEach(async ({ page }) => {
await resetDatabaseAndImportDataFromPath('./e2e/data/with-admin.tar');
await page.goto('/admin');
await login({ page });
});

test('A user should be able create, edit, or publish/unpublish an entry, navigate to the history page, and select versions to view from a list', async ({
page,
}) => {
const HISTORY_URL =
/\/admin\/content-manager\/single-types\/api::homepage.homepage\/history(\?.*)?/;

// Navigate to the content-manager - single type - homepage
await page.getByRole('link', { name: 'Content Manager' }).click();
await page.getByRole('link', { name: 'Homepage' }).click();
await page.getByRole('combobox', { name: 'Locales' }).click();
await page.getByRole('option', { name: 'French (fr)' }).click();

/**
* Create
*/
const titleInput = await page.getByRole('textbox', { name: 'title' });
// Create a french version
const frenchTitle = 'Paris Saint-Germain';
await titleInput.fill(frenchTitle);
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('link', { name: 'History' }).click();
await page.waitForURL(HISTORY_URL);
await expect(titleInput).toHaveValue(frenchTitle);

// Go back to the CM to create a new english entry
await page.getByRole('link', { name: 'Back' }).click();
await page.getByRole('combobox', { name: 'Locales' }).click();
await page.getByRole('option', { name: 'English (en)' }).click();

// Create an english version
const englishTitle = 'AFC Richmond';
await titleInput.fill(englishTitle);
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('link', { name: 'History' }).click();
await page.waitForURL(HISTORY_URL);
const versionCards = await page.getByRole('listitem', { name: 'Version card' });
await expect(versionCards).toHaveCount(1);
// Assert the id was added after page load
const idRegex = /id=\d+/;
await expect(idRegex.test(page.url())).toBe(true);
// Assert the most recent version is the current version
const currentVersion = versionCards.nth(0);
await expect(currentVersion.getByText('(current)')).toBeVisible();
await expect(currentVersion.getByText('Draft')).toBeVisible();
await expect(titleInput).toBeDisabled();
await expect(titleInput).toHaveValue(englishTitle);
// Assert only the english versions are available
await expect(page.getByText(frenchTitle)).not.toBeVisible();

// Go back to the entry
await page.getByRole('link', { name: 'Back' }).click();

/**
* Update
*/
await page.getByRole('textbox', { name: 'title' }).fill('Welcome to AFC Richmond');
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('link', { name: 'History' }).click();
await expect(versionCards).toHaveCount(2);
// Assert the most recent version is the current version
versionCards.first().click();
await expect(titleInput).toHaveValue('Welcome to AFC Richmond');
// Assert the previous version in the list is the expected version
versionCards.nth(1).click();
await expect(titleInput).toHaveValue('AFC Richmond');

// Go back to the entry
// await page.getByRole('link', { name: 'Back' }).click();

/**
* Publish
*
* TODO: Fix publish
* The publish action in the middleware used to create history versions has a different shape than the other actions.
* This leaves us with null for status and relatedDocumentId in the history version.
*
*/
// await page.getByRole('button', { name: 'Publish' }).click();
// await page.getByRole('link', { name: 'History' }).click();
// await page.waitForURL('**/content-manager/single-types/api::homepage.homepage/history**');
// await expect(versionCards).toHaveCount(3);
// // Assert the current version is the most recent published version
// await expect(titleInput).toHaveValue('Welcome to AFC Richmond!');
// // TODO: Assert the version is marked as published when publishing works
// await expect(currentVersion.getByText('Published')).toBeVisible();
// // Assert the previous version in the list is the expected version
// await expect(versionCards.nth(1).getByText('Draft')).toBeVisible();
// versionCards.nth(1).click();
// await expect(titleInput).toHaveValue('Welcome to AFC Richmond');
});
});
});
3 changes: 3 additions & 0 deletions e2e/tests/content-releases/release-details-page.spec.ts
Expand Up @@ -27,6 +27,9 @@ const addEntryToRelease = async ({ page, releaseName }: { page: Page; releaseNam
).toBeVisible();
};

/**
* Skip tests on v5 until Releases + Scheduling are migrated to v5
*/
describeOnCondition(/*edition === 'EE'*/ false)('Release page', () => {
test.beforeEach(async ({ page }) => {
await resetDatabaseAndImportDataFromPath('./e2e/data/with-admin.tar');
Expand Down
1 change: 1 addition & 0 deletions examples/getstarted/config/features.js
@@ -1,5 +1,6 @@
module.exports = ({ env }) => ({
future: {
history: env.bool('STRAPI_FEATURES_FUTURE_CONTENT_HISTORY', false),
contentReleasesScheduling: env.bool('STRAPI_FUTURE_CONTENT_RELEASES_SCHEDULING', false),
},
});
8 changes: 8 additions & 0 deletions packages/core/admin/admin/src/StrapiApp.tsx
Expand Up @@ -26,6 +26,7 @@ import {
import { Page } from './components/PageHelpers';
import { Providers } from './components/Providers';
import { HOOKS } from './constants';
import { InjectedLink } from './content-manager/history/components/InjectedLink';
import { routes as cmRoutes } from './content-manager/router';
import { ContentManagerPlugin } from './core/apis/content-manager';
import { CustomFields } from './core/apis/CustomFields';
Expand Down Expand Up @@ -351,6 +352,13 @@ class StrapiApp {
}
});

// TODO: remove once we can add the link via a document action instead
this.injectContentManagerComponent('editView', 'right-links', {
name: 'history',
Component: InjectedLink,
slug: 'history',
});

if (isFunction(customBootstrap)) {
customBootstrap({
addComponents: this.addComponents,
Expand Down
3 changes: 2 additions & 1 deletion packages/core/admin/admin/src/components/FormInputs/Date.tsx
Expand Up @@ -16,6 +16,7 @@ const DateInput = forwardRef<HTMLInputElement, InputProps>(
const fieldRef = useFocusInputField(name);

const composedRefs = useComposedRefs<HTMLInputElement | null>(ref, fieldRef);
const value = typeof field.value === 'string' ? new Date(field.value) : field.value;

return (
<DatePicker
Expand All @@ -34,7 +35,7 @@ const DateInput = forwardRef<HTMLInputElement, InputProps>(
onClear={() => field.onChange(name, undefined)}
placeholder={placeholder}
required={required}
selectedDate={field.value}
selectedDate={value}
/>
);
}
Expand Down
Expand Up @@ -16,6 +16,7 @@ const DateTimeInput = forwardRef<HTMLInputElement, InputProps>(
const fieldRef = useFocusInputField(name);

const composedRefs = useComposedRefs<HTMLInputElement | null>(ref, fieldRef);
const value = typeof field.value === 'string' ? new Date(field.value) : field.value;

return (
<DateTimePicker
Expand All @@ -34,7 +35,7 @@ const DateTimeInput = forwardRef<HTMLInputElement, InputProps>(
onClear={() => field.onChange(name, undefined)}
placeholder={placeholder}
required={required}
value={field.value}
value={value}
/>
);
}
Expand Down
@@ -0,0 +1 @@
If you or the company you represent has entered into a written agreement referencing the Enterprise Edition of the Strapi source code available at https://github.com/strapi/strapi, then such agreement applies to your use of the Enterprise Edition of the Strapi Software. If you or the company you represent is using the Enterprise Edition of the Strapi Software in connection with a subscription to our cloud offering, then the agreement you have agreed to with respect to our cloud offering and the licenses included in such agreement apply to your use of the Enterprise Edition of the Strapi Software. Otherwise, the Strapi Enterprise Software License Agreement (found here https://strapi.io/enterprise-terms) applies to your use of the Enterprise Edition of the Strapi Software. BY ACCESSING OR USING THE ENTERPRISE EDITION OF THE STRAPI SOFTWARE, YOU ARE AGREEING TO BE BOUND BY THE RELEVANT REFERENCED AGREEMENT. IF YOU ARE NOT AUTHORIZED TO ACCEPT THESE TERMS ON BEHALF OF THE COMPANY YOU REPRESENT OR IF YOU DO NOT AGREE TO ALL OF THE RELEVANT TERMS AND CONDITIONS REFERENCED AND YOU HAVE NOT OTHERWISE EXECUTED A WRITTEN AGREEMENT WITH STRAPI, YOU ARE NOT AUTHORIZED TO ACCESS OR USE OR ALLOW ANY USER TO ACCESS OR USE ANY PART OF THE ENTERPRISE EDITION OF THE STRAPI SOFTWARE. YOUR ACCESS RIGHTS ARE CONDITIONAL ON YOUR CONSENT TO THE RELEVANT REFERENCED TERMS TO THE EXCLUSION OF ALL OTHER TERMS; IF THE RELEVANT REFERENCED TERMS ARE CONSIDERED AN OFFER BY YOU, ACCEPTANCE IS EXPRESSLY LIMITED TO THE RELEVANT REFERENCED TERMS.
@@ -0,0 +1,25 @@
import * as React from 'react';

import { LinkButton } from '@strapi/design-system/v2';
import { useQueryParams } from '@strapi/helper-plugin';
import { stringify } from 'qs';
import { NavLink } from 'react-router-dom';

/**
* This is a temporary component to easily access the history page.
* TODO: delete it when the document actions API is ready
*/

const InjectedLink = () => {
const [{ query }] = useQueryParams<{ plugins?: Record<string, unknown> }>();
const pluginsQueryParams = stringify({ plugins: query.plugins }, { encode: false });

return (
// @ts-expect-error - types are not inferred correctly through the as prop.
<LinkButton as={NavLink} variant="primary" to={`history?${pluginsQueryParams}`}>
History
</LinkButton>
);
};

export { InjectedLink };

0 comments on commit d28dfbf

Please sign in to comment.