Fix types and login logo, remove unneeded files

This commit is contained in:
Daniel Lautzenheiser 2022-09-30 08:20:36 -04:00
parent 8acda23acc
commit 0fd69879fa
29 changed files with 83 additions and 6961 deletions

View File

@ -71,10 +71,9 @@ Simple CMS uses the [Forking Workflow](https://www.atlassian.com/git/tutorials/c
2. Create a branch from `main`. If you're addressing a specific issue, prefix your branch name with the issue number.
3. If you've added code that should be tested, add tests.
4. If you've changed APIs, update the documentation.
5. Run `yarn test` and ensure the test suite passes.
6. Use `yarn format` to format and lint your code.
7. PR's must be rebased before merge (feel free to ask for help).
8. PR should be reviewed by two maintainers prior to merging.
5. Use `yarn format` to format and lint your code.
6. PR's must be rebased before merge (feel free to ask for help).
7. PR should be reviewed by two maintainers prior to merging.
## Debugging
@ -105,34 +104,6 @@ When debugging the CMS with Git Gateway you must:
4. Refresh the page
5. You should be able to log in via your Netlify Identity email/password
### Fine tune the way you run unit tests
There are situations where you would want to run a specific test file, or tests that match a certain pattern.
To run all the tests for a specific file, use this command:
```
yarn jest <filename or file path>
```
The first part of the command, `yarn jest` means running the locally installed version of `jest`. It is equivalent to running `node_modules/.bin/jest`.
Example for running all the tests for the file `gitlab.spec.js`: `yarn jest gitlab.spec.js`
Example for running all the tests for the file `API.spec.js` in the `gitlab` package:
`yarn jest ".+backend-gitlab/.+/API.spec.js`
To run a specific test in a file, add the flag `--testNamePattern`, or `-t` for short followed by a regexp to match your test name.
Example for running the test "should return true on project access_level >= 30" in the API.spec.js in `gitlab` package:
```
yarn jest -t "true on p" ".+backend-gitlab/.+/API.spec.js"
```
For more information about running tests exactly the way you want, check out the official documentation for [Jest CLI](https://jestjs.io/docs/cli).
## License
By contributing to Simple CMS, you agree that your contributions will be licensed under its [MIT license](LICENSE).

View File

@ -125,4 +125,8 @@ Every release is documented on the Github [Releases](https://github.com/SimpleCM
Simple CMS is released under the [MIT License](LICENSE).
Please make sure you understand its [implications and guarantees](https://writing.kemitchell.com/2016/09/21/MIT-License-Line-by-Line.html).
# Netlify CMS
Simple CMS is a fork of Netlify CMS focusing on the core product over adding massive new features.
# Thanks

View File

@ -1,9 +0,0 @@
{
"baseUrl": "http://localhost:8080",
"projectId": "dzqjxb",
"testFiles": "*spec*.js",
"retries": {
"runMode": 2,
"openMode": 0
}
}

View File

@ -1,84 +0,0 @@
const crypto = require('crypto');
const axios = require('axios');
const verifySignature = event => {
const timestamp = Number(event.headers['x-slack-request-timestamp']);
const time = Math.floor(Date.now() / 1000);
if (time - timestamp > 60 * 5) {
throw new Error(`Failed verifying signature. Timestamp too old '${timestamp}'`);
}
const body = event.body;
const sigString = `v0:${timestamp}:${body}`;
const actualSignature = event.headers['x-slack-signature'];
const secret = process.env.SLACK_SIGNING_SECRET;
const hash = crypto
.createHmac('sha256', secret)
.update(sigString, 'utf8')
.digest('hex');
const expectedSignature = `v0=${hash}`;
const signaturesMatch = crypto.timingSafeEqual(
Buffer.from(actualSignature, 'utf8'),
Buffer.from(expectedSignature, 'utf8'),
);
if (!signaturesMatch) {
throw new Error(
`Signatures don't match. Expected: '${expectedSignature}', actual: '${actualSignature}'`,
);
}
};
exports.handler = async function(event) {
try {
console.info(
JSON.stringify(
{
event,
env: {
PUBLISH_COMMAND: process.env.PUBLISH_COMMAND,
ALLOWED_USERS: process.env.ALLOWED_USERS,
GITHUB_REPO: process.env.GITHUB_REPO,
},
},
null,
2,
),
);
verifySignature(event);
const params = new URLSearchParams(event.body);
const command = params.get('command');
const userId = params.get('user_id');
const allowedUsers = (process.env.ALLOWED_USERS || '').split(',');
if (!allowedUsers.includes(userId)) {
throw new Error(`User '${params.get('user_name')}' is not allowed to run command`);
}
const expectedCommand = process.env.PUBLISH_COMMAND;
if (expectedCommand && expectedCommand == command) {
const githubToken = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPO;
await axios({
headers: { Authorization: `token ${githubToken}` },
method: 'post',
url: `https://api.github.com/repos/${repo}/dispatches`,
data: { event_type: 'on-demand-github-action' },
});
const message = 'Dispatched event to GitHub';
return { status: 200, body: message };
} else {
throw new Error(`Command is not allowed. Expected: ${expectedCommand}. Actual: ${command}`);
}
} catch (e) {
console.info(e);
const response = {
body: 'Unauthorized',
status: 401,
};
return response;
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

2
index.d.ts vendored
View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module 'simple-cms-core' {
declare module '@simplecms/simple-cms-core' {
import type { Iterable as ImmutableIterable, List, Map } from 'immutable';
import type { ComponentType, FocusEventHandler, ReactNode } from 'react';
import type { t } from 'react-polyglot';

View File

@ -1,12 +0,0 @@
module.exports = {
setupFilesAfterEnv: ['<rootDir>/setupTestFramework.js'],
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
},
testURL: 'http://localhost:8080',
snapshotSerializers: ['jest-emotion'],
transformIgnorePatterns: [
'node_modules/(?!copy-text-to-clipboard|clean-stack|escape-string-regexp)',
],
testEnvironment: 'jsdom',
};

View File

@ -1,6 +1,6 @@
{
"name": "@simplecms/simple-cms-core",
"version": "0.1.0",
"version": "0.1.2",
"license": "MIT",
"description": "Simple CMS core application.",
"repository": "https://github.com/SimpleCMS/simple-cms",
@ -169,12 +169,8 @@
"@emotion/eslint-plugin": "11.10.0",
"@octokit/rest": "16.43.2",
"@stylelint/postcss-css-in-js": "0.37.3",
"@testing-library/dom": "8.18.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@types/common-tags": "1.8.0",
"@types/history": "4.7.11",
"@types/jest": "27.5.2",
"@types/js-base64": "3.3.1",
"@types/jwt-decode": "2.2.1",
"@types/lodash": "4.14.185",
@ -189,7 +185,6 @@
"axios": "0.26.1",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "11.0.0-beta.2",
"babel-jest": "27.5.1",
"babel-loader": "8.2.5",
"babel-plugin-emotion": "11.0.0",
"babel-plugin-inline-json-import": "0.3.2",
@ -204,11 +199,6 @@
"commonmark-spec": "0.30.0",
"cross-env": "7.0.3",
"css-loader": "3.6.0",
"cypress": "9.5.3",
"cypress-file-upload": "5.0.8",
"cypress-image-snapshot": "4.0.1",
"cypress-jest-adapter": "0.1.1",
"cypress-plugin-tab": "1.0.5",
"dotenv": "10.0.0",
"eslint": "8.24.0",
"eslint-plugin-cypress": "2.12.1",
@ -220,9 +210,6 @@
"fs-extra": "10.1.0",
"gitlab": "14.2.2",
"http-server": "14.1.1",
"jest": "27.5.1",
"jest-cli": "27.5.1",
"jest-emotion": "11.0.0",
"js-yaml": "4.1.0",
"mockserver-client": "5.14.0",
"mockserver-node": "5.14.0",
@ -234,13 +221,11 @@
"postcss-scss": "4.0.5",
"prettier": "2.7.1",
"react-svg-loader": "3.0.3",
"react-test-renderer": "16.14.0",
"rehype": "7.0.0",
"rimraf": "3.0.2",
"simple-git": "3.14.1",
"slate-hyperscript": "0.13.9",
"source-map-loader": "^4.0.0",
"start-server-and-test": "1.14.0",
"stylelint": "14.12.1",
"stylelint-config-standard-scss": "3.0.0",
"stylelint-config-styled-components": "0.1.1",

View File

@ -1,45 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import { CopyToClipBoardButton } from '../MediaLibraryButtons';
describe('CopyToClipBoardButton', () => {
const props = {
disabled: false,
t: jest.fn(key => key),
};
it('should use copy text when no path is defined', () => {
const { container } = render(<CopyToClipBoardButton {...props} />);
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copy');
});
it('should use copyUrl text when path is absolute and is draft', () => {
const { container } = render(
<CopyToClipBoardButton {...props} path="https://www.images.com/image.png" draft />,
);
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyUrl');
});
it('should use copyUrl text when path is absolute and is not draft', () => {
const { container } = render(
<CopyToClipBoardButton {...props} path="https://www.images.com/image.png" />,
);
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyUrl');
});
it('should use copyName when path is not absolute and is draft', () => {
const { container } = render(<CopyToClipBoardButton {...props} path="image.png" draft />);
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyName');
});
it('should use copyPath when path is not absolute and is not draft', () => {
const { container } = render(<CopyToClipBoardButton {...props} path="image.png" />);
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyPath');
});
});

View File

@ -1,49 +0,0 @@
import React from 'react';
import { Map } from 'immutable';
import { render } from '@testing-library/react';
import MediaLibraryCard from '../MediaLibraryCard';
describe('MediaLibraryCard', () => {
const props = {
displayURL: Map({ url: 'url' }),
text: 'image.png',
onClick: jest.fn(),
draftText: 'Draft',
width: '100px',
height: '240px',
margin: '10px',
isViewableImage: true,
loadDisplayURL: jest.fn(),
};
it('should match snapshot for non draft image', () => {
const { asFragment, queryByTestId } = render(<MediaLibraryCard {...props} />);
expect(queryByTestId('draft-text')).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
it('should match snapshot for draft image', () => {
const { asFragment, getByTestId } = render(<MediaLibraryCard {...props} isDraft={true} />);
expect(getByTestId('draft-text')).toHaveTextContent('Draft');
expect(asFragment()).toMatchSnapshot();
});
it('should match snapshot for non viewable image', () => {
const { asFragment, getByTestId } = render(
<MediaLibraryCard {...props} isViewableImage={false} type="Not Viewable" />,
);
expect(getByTestId('card-file-icon')).toHaveTextContent('Not Viewable');
expect(asFragment()).toMatchSnapshot();
});
it('should call loadDisplayURL on mount when url is empty', () => {
const loadDisplayURL = jest.fn();
render(
<MediaLibraryCard {...props} loadDisplayURL={loadDisplayURL} displayURL={Map({ url: '' })} />,
);
expect(loadDisplayURL).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,214 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MediaLibraryCard should match snapshot for draft image 1`] = `
<DocumentFragment>
.emotion-8 {
width: 100px;
height: 240px;
margin: 10px;
border: solid 2px #dfdfe3;
border-radius: 5px;
cursor: pointer;
overflow: hidden;
}
.emotion-8:focus {
outline: none;
}
.emotion-4 {
height: 162px;
background-color: #f2f2f2;
background-size: 16px 16px;
background-position: 0 0,8px 8px;
background-image: linear-gradient( 45deg, #e6e6e6 25%, transparent 25%, transparent 75%, #e6e6e6 75%, #e6e6e6 ) , linear-gradient( 45deg, #e6e6e6 25%, transparent 25%, transparent 75%, #e6e6e6 75%, #e6e6e6 );
box-shadow: inset 0 0 4px rgba(68,74,87,0.3);
border-bottom: solid 2px #dfdfe3;
position: relative;
}
.emotion-2 {
width: 100%;
height: 160px;
object-fit: contain;
border-radius: 2px 2px 0 0;
}
.emotion-6 {
color: #798291;
padding: 8px;
margin-top: 20px;
overflow-wrap: break-word;
line-height: 1.3 !important;
}
.emotion-0 {
color: #70399f;
background-color: #f6d8ff;
position: absolute;
padding: 8px;
border-radius: 5px 0 5px 0;
}
<div
class="emotion-8 emotion-9"
height="240px"
tabindex="-1"
width="100px"
>
<div
class="emotion-4 emotion-5"
>
<p
class="emotion-0 emotion-1"
data-testid="draft-text"
>
Draft
</p>
<img
class="emotion-2 emotion-3"
src="url"
/>
</div>
<p
class="emotion-6 emotion-7"
>
image.png
</p>
</div>
</DocumentFragment>
`;
exports[`MediaLibraryCard should match snapshot for non draft image 1`] = `
<DocumentFragment>
.emotion-6 {
width: 100px;
height: 240px;
margin: 10px;
border: solid 2px #dfdfe3;
border-radius: 5px;
cursor: pointer;
overflow: hidden;
}
.emotion-6:focus {
outline: none;
}
.emotion-2 {
height: 162px;
background-color: #f2f2f2;
background-size: 16px 16px;
background-position: 0 0,8px 8px;
background-image: linear-gradient( 45deg, #e6e6e6 25%, transparent 25%, transparent 75%, #e6e6e6 75%, #e6e6e6 ) , linear-gradient( 45deg, #e6e6e6 25%, transparent 25%, transparent 75%, #e6e6e6 75%, #e6e6e6 );
box-shadow: inset 0 0 4px rgba(68,74,87,0.3);
border-bottom: solid 2px #dfdfe3;
position: relative;
}
.emotion-0 {
width: 100%;
height: 160px;
object-fit: contain;
border-radius: 2px 2px 0 0;
}
.emotion-4 {
color: #798291;
padding: 8px;
margin-top: 20px;
overflow-wrap: break-word;
line-height: 1.3 !important;
}
<div
class="emotion-6 emotion-7"
height="240px"
tabindex="-1"
width="100px"
>
<div
class="emotion-2 emotion-3"
>
<img
class="emotion-0 emotion-1"
src="url"
/>
</div>
<p
class="emotion-4 emotion-5"
>
image.png
</p>
</div>
</DocumentFragment>
`;
exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = `
<DocumentFragment>
.emotion-6 {
width: 100px;
height: 240px;
margin: 10px;
border: solid 2px #dfdfe3;
border-radius: 5px;
cursor: pointer;
overflow: hidden;
}
.emotion-6:focus {
outline: none;
}
.emotion-2 {
height: 162px;
background-color: #f2f2f2;
background-size: 16px 16px;
background-position: 0 0,8px 8px;
background-image: linear-gradient( 45deg, #e6e6e6 25%, transparent 25%, transparent 75%, #e6e6e6 75%, #e6e6e6 ) , linear-gradient( 45deg, #e6e6e6 25%, transparent 25%, transparent 75%, #e6e6e6 75%, #e6e6e6 );
box-shadow: inset 0 0 4px rgba(68,74,87,0.3);
border-bottom: solid 2px #dfdfe3;
position: relative;
}
.emotion-4 {
color: #798291;
padding: 8px;
margin-top: 20px;
overflow-wrap: break-word;
line-height: 1.3 !important;
}
.emotion-0 {
width: 100%;
height: 160px;
object-fit: cover;
border-radius: 2px 2px 0 0;
padding: 1em;
font-size: 3em;
}
<div
class="emotion-6 emotion-7"
height="240px"
tabindex="-1"
width="100px"
>
<div
class="emotion-2 emotion-3"
>
<div
class="emotion-0 emotion-1"
data-testid="card-file-icon"
>
Not Viewable
</div>
</div>
<p
class="emotion-4 emotion-5"
>
image.png
</p>
</div>
</DocumentFragment>
`;

View File

@ -1,511 +0,0 @@
import { merge } from 'lodash';
import { validateConfig } from '../configSchema';
jest.mock('../../lib/registry');
describe('config', () => {
/**
* Suppress error logging to reduce noise during testing. Jest will still
* log test failures and associated errors as expected.
*/
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});
const { getWidgets } = require('../../lib/registry');
getWidgets.mockImplementation(() => [{}]);
describe('validateConfig', () => {
const validConfig = {
foo: 'bar',
backend: { name: 'bar' },
media_folder: 'baz',
collections: [
{
name: 'posts',
label: 'Posts',
folder: '_posts',
fields: [{ name: 'title', label: 'title', widget: 'string' }],
},
],
};
it('should not throw if no errors', () => {
expect(() => {
validateConfig(validConfig);
}).not.toThrowError();
});
it('should throw if backend is not defined in config', () => {
expect(() => {
validateConfig({ foo: 'bar' });
}).toThrowError("config must have required property 'backend'");
});
it('should throw if backend name is not defined in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: {} });
}).toThrowError("'backend' must have required property 'name'");
});
it('should throw if backend name is not a string in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: {} } });
}).toThrowError("'backend.name' must be string");
});
it('should throw if backend.open_authoring is not a boolean in config', () => {
expect(() => {
validateConfig(merge(validConfig, { backend: { open_authoring: 'true' } }));
}).toThrowError("'backend.open_authoring' must be boolean");
});
it('should not throw if backend.open_authoring is boolean in config', () => {
expect(() => {
validateConfig(merge(validConfig, { backend: { open_authoring: true } }));
}).not.toThrowError();
});
it('should throw if backend.auth_scope is not "repo" or "public_repo" in config', () => {
expect(() => {
validateConfig(merge(validConfig, { backend: { auth_scope: 'user' } }));
}).toThrowError("'backend.auth_scope' must be equal to one of the allowed values");
});
it('should not throw if backend.auth_scope is one of "repo" or "public_repo" in config', () => {
expect(() => {
validateConfig(merge(validConfig, { backend: { auth_scope: 'repo' } }));
}).not.toThrowError();
expect(() => {
validateConfig(merge(validConfig, { backend: { auth_scope: 'public_repo' } }));
}).not.toThrowError();
});
it('should throw if media_folder is not defined in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' } });
}).toThrowError("config must have required property 'media_folder'");
});
it('should throw if media_folder is not a string in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: {} });
}).toThrowError("'media_folder' must be string");
});
it('should throw if collections is not defined in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz' });
}).toThrowError("config must have required property 'collections'");
});
it('should throw if collections not an array in config', () => {
expect(() => {
validateConfig({
foo: 'bar',
backend: { name: 'bar' },
media_folder: 'baz',
collections: {},
});
}).toThrowError("'collections' must be array");
});
it('should throw if collections is an empty array in config', () => {
expect(() => {
validateConfig({
foo: 'bar',
backend: { name: 'bar' },
media_folder: 'baz',
collections: [],
});
}).toThrowError("'collections' must NOT have fewer than 1 items");
});
it('should throw if collections is an array with a single null element in config', () => {
expect(() => {
validateConfig({
foo: 'bar',
backend: { name: 'bar' },
media_folder: 'baz',
collections: [null],
});
}).toThrowError("'collections[0]' must be object");
});
it('should throw if local_backend is not a boolean or plain object', () => {
expect(() => {
validateConfig({ ...validConfig, local_backend: [] });
}).toThrowError("'local_backend' must be boolean");
});
it('should throw if local_backend url is not a string', () => {
expect(() => {
validateConfig({ ...validConfig, local_backend: { url: [] } });
}).toThrowError("'local_backend.url' must be string");
});
it('should throw if local_backend allowed_hosts is not a string array', () => {
expect(() => {
validateConfig({ ...validConfig, local_backend: { allowed_hosts: [true] } });
}).toThrowError("'local_backend.allowed_hosts[0]' must be string");
});
it('should not throw if local_backend is a boolean', () => {
expect(() => {
validateConfig({ ...validConfig, local_backend: true });
}).not.toThrowError();
});
it('should not throw if local_backend is a plain object with url string property', () => {
expect(() => {
validateConfig({ ...validConfig, local_backend: { url: 'http://localhost:8081/api/v1' } });
}).not.toThrowError();
});
it('should not throw if local_backend is a plain object with allowed_hosts string array property', () => {
expect(() => {
validateConfig({
...validConfig,
local_backend: { allowed_hosts: ['192.168.0.1'] },
});
}).not.toThrowError();
});
it('should throw if collection publish is not a boolean', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ publish: 'false' }] }));
}).toThrowError("'collections[0].publish' must be boolean");
});
it('should not throw if collection publish is a boolean', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ publish: false }] }));
}).not.toThrowError();
});
it('should throw if collections sortable_fields is not a boolean or a string array', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortable_fields: 'title' }] }));
}).toThrowError("'collections[0].sortable_fields' must be array");
});
it('should allow sortable_fields to be a string array', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortable_fields: ['title'] }] }));
}).not.toThrow();
});
it('should allow sortable_fields to be a an empty array', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortable_fields: [] }] }));
}).not.toThrow();
});
it('should allow sortableFields instead of sortable_fields', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ sortableFields: [] }] }));
}).not.toThrow();
});
it('should throw if both sortable_fields and sortableFields exist', () => {
expect(() => {
validateConfig(
merge({}, validConfig, { collections: [{ sortable_fields: [], sortableFields: [] }] }),
);
}).toThrowError("'collections[0]' must NOT be valid");
});
it('should throw if collection names are not unique', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [validConfig.collections[0], validConfig.collections[0]],
}),
);
}).toThrowError("'collections' collections names must be unique");
});
it('should throw if collection file names are not unique', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [
{},
{
files: [
{
name: 'a',
label: 'a',
file: 'a.md',
fields: [{ name: 'title', label: 'title', widget: 'string' }],
},
{
name: 'a',
label: 'b',
file: 'b.md',
fields: [{ name: 'title', label: 'title', widget: 'string' }],
},
],
},
],
}),
);
}).toThrowError("'collections[1].files' files names must be unique");
});
it('should throw if collection fields names are not unique', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [
{
fields: [
{ name: 'title', label: 'title', widget: 'string' },
{ name: 'title', label: 'other title', widget: 'string' },
],
},
],
}),
);
}).toThrowError("'collections[0].fields' fields names must be unique");
});
it('should not throw if collection fields are unique across nesting levels', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [
{
fields: [
{ name: 'title', label: 'title', widget: 'string' },
{
name: 'object',
label: 'Object',
widget: 'object',
fields: [{ name: 'title', label: 'title', widget: 'string' }],
},
],
},
],
}),
);
}).not.toThrow();
});
describe('nested validation', () => {
const { getWidgets } = require('../../lib/registry');
getWidgets.mockImplementation(() => [
{
name: 'relation',
schema: {
properties: {
search_fields: { type: 'array', items: { type: 'string' } },
display_fields: { type: 'array', items: { type: 'string' } },
},
},
},
]);
it('should throw if nested relation display_fields and search_fields are not arrays', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [
{
fields: [
{ name: 'title', label: 'title', widget: 'string' },
{
name: 'object',
label: 'Object',
widget: 'object',
fields: [
{ name: 'title', label: 'title', widget: 'string' },
{
name: 'relation',
label: 'relation',
widget: 'relation',
display_fields: 'title',
search_fields: 'title',
},
],
},
],
},
],
}),
);
}).toThrowError(
"'collections[0].fields[1].fields[1].search_fields' must be array\n'collections[0].fields[1].fields[1].display_fields' must be array",
);
});
it('should not throw if nested relation display_fields and search_fields are arrays', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [
{
fields: [
{ name: 'title', label: 'title', widget: 'string' },
{
name: 'object',
label: 'Object',
widget: 'object',
fields: [
{ name: 'title', label: 'title', widget: 'string' },
{
name: 'relation',
label: 'relation',
widget: 'relation',
display_fields: ['title'],
search_fields: ['title'],
},
],
},
],
},
],
}),
);
}).not.toThrow();
});
});
it('should throw if collection meta is not a plain object', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ meta: [] }] }));
}).toThrowError("'collections[0].meta' must be object");
});
it('should throw if collection meta is an empty object', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ meta: {} }] }));
}).toThrowError("'collections[0].meta' must NOT have fewer than 1 items");
});
it('should throw if collection meta is an empty object', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ meta: { path: {} } }] }));
}).toThrowError("'collections[0].meta.path' must have required property 'label'");
expect(() => {
validateConfig(
merge({}, validConfig, { collections: [{ meta: { path: { label: 'Label' } } }] }),
);
}).toThrowError("'collections[0].meta.path' must have required property 'widget'");
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [{ meta: { path: { label: 'Label', widget: 'widget' } } }],
}),
);
}).toThrowError("'collections[0].meta.path' must have required property 'index_file'");
});
it('should allow collection meta to have a path configuration', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [
{ meta: { path: { label: 'Path', widget: 'string', index_file: 'index' } } },
],
}),
);
}).not.toThrow();
});
it('should throw if collection field pattern is not an array', () => {
expect(() => {
validateConfig(merge({}, validConfig, { collections: [{ fields: [{ pattern: '' }] }] }));
}).toThrowError("'collections[0].fields[0].pattern' must be array");
});
it('should throw if collection field pattern is not an array of [string|regex, string]', () => {
expect(() => {
validateConfig(
merge({}, validConfig, { collections: [{ fields: [{ pattern: [1, ''] }] }] }),
);
}).toThrowError(
"'collections[0].fields[0].pattern[0]' must be string\n'collections[0].fields[0].pattern[0]' must be a regular expression",
);
expect(() => {
validateConfig(
merge({}, validConfig, { collections: [{ fields: [{ pattern: ['', 1] }] }] }),
);
}).toThrowError("'collections[0].fields[0].pattern[1]' must be string");
});
it('should allow collection field pattern to be an array of [string|regex, string]', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [{ fields: [{ pattern: ['pattern', 'error'] }] }],
}),
);
}).not.toThrow();
expect(() => {
validateConfig(
merge({}, validConfig, {
collections: [{ fields: [{ pattern: [/pattern/, 'error'] }] }],
}),
);
}).not.toThrow();
});
describe('i18n', () => {
it('should throw error when locale has invalid characters', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
i18n: {
structure: 'multiple_folders',
locales: ['en', 'tr.TR'],
},
}),
);
}).toThrowError(`'i18n.locales[1]' must match pattern "^[a-zA-Z-_]+$"`);
});
it('should throw error when locale is less than 2 characters', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
i18n: {
structure: 'multiple_folders',
locales: ['en', 't'],
},
}),
);
}).toThrowError(`'i18n.locales[1]' must NOT have fewer than 2 characters`);
});
it('should throw error when locale is more than 10 characters', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
i18n: {
structure: 'multiple_folders',
locales: ['en', 'a_very_long_locale'],
},
}),
);
}).toThrowError(`'i18n.locales[1]' must NOT have more than 10 characters`);
});
it('should allow valid locales strings', () => {
expect(() => {
validateConfig(
merge({}, validConfig, {
i18n: {
structure: 'multiple_folders',
locales: ['en', 'tr-TR', 'zh_CHS'],
},
}),
);
}).not.toThrow();
});
});
});
});

View File

@ -1,429 +0,0 @@
import {
FrontmatterInfer,
frontmatterJSON,
frontmatterTOML,
frontmatterYAML,
} from '../frontmatter';
describe('Frontmatter', () => {
describe('yaml', () => {
it('should parse YAML with --- delimiters', () => {
expect(
FrontmatterInfer.fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent'),
).toEqual({
title: 'YAML',
description: 'Something longer',
body: 'Content',
});
});
it('should parse YAML with --- delimiters when it is explicitly set as the format without a custom delimiter', () => {
expect(
frontmatterYAML().fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent'),
).toEqual({
title: 'YAML',
description: 'Something longer',
body: 'Content',
});
});
it('should parse YAML with custom delimiters when it is explicitly set as the format with a custom delimiter', () => {
expect(
frontmatterYAML('~~~').fromFile(
'~~~\ntitle: YAML\ndescription: Something longer\n~~~\nContent',
),
).toEqual({
title: 'YAML',
description: 'Something longer',
body: 'Content',
});
});
it('should parse YAML with custom delimiters when it is explicitly set as the format with different custom delimiters', () => {
expect(
frontmatterYAML(['~~~', '^^^']).fromFile(
'~~~\ntitle: YAML\ndescription: Something longer\n^^^\nContent',
),
).toEqual({
title: 'YAML',
description: 'Something longer',
body: 'Content',
});
});
it('should parse YAML with ---yaml delimiters', () => {
expect(
FrontmatterInfer.fromFile(
'---yaml\ntitle: YAML\ndescription: Something longer\n---\nContent',
),
).toEqual({
title: 'YAML',
description: 'Something longer',
body: 'Content',
});
});
it('should overwrite any body param in the front matter', () => {
expect(
FrontmatterInfer.fromFile('---\ntitle: The Title\nbody: Something longer\n---\nContent'),
).toEqual({
title: 'The Title',
body: 'Content',
});
});
it('should stringify YAML with --- delimiters', () => {
expect(
FrontmatterInfer.toFile({
body: 'Some content\nOn another line',
tags: ['front matter', 'yaml'],
title: 'YAML',
}),
).toEqual(
[
'---',
'tags:',
' - front matter',
' - yaml',
'title: YAML',
'---',
'Some content',
'On another line',
].join('\n'),
);
});
it('should stringify YAML with missing body', () => {
expect(FrontmatterInfer.toFile({ tags: ['front matter', 'yaml'], title: 'YAML' })).toEqual(
['---', 'tags:', ' - front matter', ' - yaml', 'title: YAML', '---', ''].join('\n'),
);
});
it('should stringify YAML with --- delimiters when it is explicitly set as the format without a custom delimiter', () => {
expect(
frontmatterYAML().toFile({
body: 'Some content\nOn another line',
tags: ['front matter', 'yaml'],
title: 'YAML',
}),
).toEqual(
[
'---',
'tags:',
' - front matter',
' - yaml',
'title: YAML',
'---',
'Some content',
'On another line',
].join('\n'),
);
});
it('should stringify YAML with --- delimiters when it is explicitly set as the format with a custom delimiter', () => {
expect(
frontmatterYAML('~~~').toFile({
body: 'Some content\nOn another line',
tags: ['front matter', 'yaml'],
title: 'YAML',
}),
).toEqual(
[
'~~~',
'tags:',
' - front matter',
' - yaml',
'title: YAML',
'~~~',
'Some content',
'On another line',
].join('\n'),
);
});
it('should stringify YAML with --- delimiters when it is explicitly set as the format with different custom delimiters', () => {
expect(
frontmatterYAML(['~~~', '^^^']).toFile({
body: 'Some content\nOn another line',
tags: ['front matter', 'yaml'],
title: 'YAML',
}),
).toEqual(
[
'~~~',
'tags:',
' - front matter',
' - yaml',
'title: YAML',
'^^^',
'Some content',
'On another line',
].join('\n'),
);
});
it('should trim last line break if added by grey-matter', () => {
expect(
frontmatterYAML().toFile({
body: 'noLineBreak',
}),
).toEqual('noLineBreak');
});
it('should not trim last line break if not added by grey-matter', () => {
expect(
frontmatterYAML().toFile({
body: 'withLineBreak\n',
}),
).toEqual('withLineBreak\n');
});
it('should keep field types', () => {
const frontmatter = frontmatterYAML();
const file = frontmatter.toFile({
number: 1,
string: 'Hello World!',
date: new Date('2020-01-01'),
array: ['1', new Date('2020-01-01')],
body: 'Content',
});
expect(frontmatter.fromFile(file)).toEqual({
number: 1,
string: 'Hello World!',
date: new Date('2020-01-01'),
array: ['1', new Date('2020-01-01')],
body: 'Content',
});
});
});
describe('toml', () => {
it('should parse TOML with +++ delimiters', () => {
expect(
FrontmatterInfer.fromFile(
'+++\ntitle = "TOML"\ndescription = "Front matter"\n+++\nContent',
),
).toEqual({
title: 'TOML',
description: 'Front matter',
body: 'Content',
});
});
it('should parse TOML with 0.5 style dates', () => {
expect(
FrontmatterInfer.fromFile('+++\ntitle = "TOML"\ndate = 2018-12-24\n+++\nContent'),
).toEqual({
title: 'TOML',
date: new Date('2018-12-24T00:00:00.000Z'),
body: 'Content',
});
});
it('should parse TOML with +++ delimiters when it is explicitly set as the format without a custom delimiter', () => {
expect(
frontmatterTOML('~~~').fromFile(
'~~~\ntitle = "TOML"\ndescription = "Front matter"\n~~~\nContent',
),
).toEqual({
title: 'TOML',
description: 'Front matter',
body: 'Content',
});
});
it('should parse TOML with ---toml delimiters', () => {
expect(
FrontmatterInfer.fromFile(
'---toml\ntitle = "TOML"\ndescription = "Something longer"\n---\nContent',
),
).toEqual({
title: 'TOML',
description: 'Something longer',
body: 'Content',
});
});
it('should stringify TOML with +++ delimiters when it is explicitly set as the format without a custom delimiter', () => {
expect(
frontmatterTOML().toFile({
body: 'Some content\nOn another line',
tags: ['front matter', 'toml'],
title: 'TOML',
}),
).toEqual(
[
'+++',
'tags = ["front matter", "toml"]',
'title = "TOML"',
'+++',
'Some content',
'On another line',
].join('\n'),
);
});
it('should stringify TOML with +++ delimiters when it is explicitly set as the format with a custom delimiter', () => {
expect(
frontmatterTOML('~~~').toFile({
body: 'Some content\nOn another line',
tags: ['front matter', 'toml'],
title: 'TOML',
}),
).toEqual(
[
'~~~',
'tags = ["front matter", "toml"]',
'title = "TOML"',
'~~~',
'Some content',
'On another line',
].join('\n'),
);
});
it('should keep field types', () => {
const frontmatter = frontmatterTOML();
const file = frontmatter.toFile({
number: 1,
string: 'Hello World!',
date: new Date('2020-01-01'),
// in toml arrays must contain the same type
array: ['1', new Date('2020-01-01').toISOString()],
body: 'Content',
});
expect(frontmatter.fromFile(file)).toEqual({
number: 1,
string: 'Hello World!',
date: new Date('2020-01-01'),
array: ['1', new Date('2020-01-01').toISOString()],
body: 'Content',
});
});
});
describe('json', () => {
it('should parse JSON with { } delimiters', () => {
expect(
FrontmatterInfer.fromFile(
'{\n"title": "The Title",\n"description": "Something longer"\n}\nContent',
),
).toEqual({
title: 'The Title',
description: 'Something longer',
body: 'Content',
});
});
it('should parse JSON with { } delimiters when it is explicitly set as the format without a custom delimiter', () => {
expect(
frontmatterJSON().fromFile(
'{\n"title": "The Title",\n"description": "Something longer"\n}\nContent',
),
).toEqual({
title: 'The Title',
description: 'Something longer',
body: 'Content',
});
});
it('should parse JSON with { } delimiters when it is explicitly set as the format with a custom delimiter', () => {
expect(
frontmatterJSON('~~~').fromFile(
'~~~\n"title": "The Title",\n"description": "Something longer"\n~~~\nContent',
),
).toEqual({
title: 'The Title',
description: 'Something longer',
body: 'Content',
});
});
it('should parse JSON with ---json delimiters', () => {
expect(
FrontmatterInfer.fromFile(
'---json\n{\n"title": "The Title",\n"description": "Something longer"\n}\n---\nContent',
),
).toEqual({
title: 'The Title',
description: 'Something longer',
body: 'Content',
});
});
it('should parse JSON with { } delimiters ending with a nested object', () => {
expect(
FrontmatterInfer.fromFile(
'{\n "title": "The Title",\n "nested": {\n "inside": "Inside prop"\n }\n}\nContent',
),
).toEqual({
title: 'The Title',
nested: { inside: 'Inside prop' },
body: 'Content',
});
});
it('should stringify JSON with { } delimiters when it is explicitly set as the format without a custom delimiter', () => {
expect(
frontmatterJSON().toFile({
body: 'Some content\nOn another line',
tags: ['front matter', 'json'],
title: 'JSON',
}),
).toEqual(
[
'{',
'"tags": [',
' "front matter",',
' "json"',
' ],',
' "title": "JSON"',
'}',
'Some content',
'On another line',
].join('\n'),
);
});
it('should stringify JSON with { } delimiters when it is explicitly set as the format with a custom delimiter', () => {
expect(
frontmatterJSON('~~~').toFile({
body: 'Some content\nOn another line',
tags: ['front matter', 'json'],
title: 'JSON',
}),
).toEqual(
[
'~~~',
'"tags": [',
' "front matter",',
' "json"',
' ],',
' "title": "JSON"',
'~~~',
'Some content',
'On another line',
].join('\n'),
);
});
it('should keep field types', () => {
const frontmatter = frontmatterJSON();
const file = frontmatter.toFile({
number: 1,
string: 'Hello World!',
// no way to represent date in JSON
date: new Date('2020-01-01').toISOString(),
array: ['1', new Date('2020-01-01').toISOString()],
body: 'Content',
});
expect(frontmatter.fromFile(file)).toEqual({
number: 1,
string: 'Hello World!',
date: new Date('2020-01-01').toISOString(),
array: ['1', new Date('2020-01-01').toISOString()],
body: 'Content',
});
});
});
});

View File

@ -1,9 +0,0 @@
import tomlFormatter from '../toml';
describe('tomlFormatter', () => {
it('should output TOML integer values without decimals', () => {
expect(tomlFormatter.toFile({ testFloat: 123.456, testInteger: 789, title: 'TOML' })).toEqual(
['testFloat = 123.456', 'testInteger = 789', 'title = "TOML"'].join('\n'),
);
});
});

View File

@ -1,162 +0,0 @@
import { stripIndent } from 'common-tags';
import yaml from '../yaml';
describe('yaml', () => {
describe('fromFile', () => {
test('loads valid yaml', () => {
expect(yaml.fromFile('[]')).toEqual([]);
const result = yaml.fromFile(stripIndent`
date: 2020-04-02T16:08:03.327Z
dateString: 2020-04-02
boolean: true
number: 1
`);
expect(result).toEqual({
date: new Date('2020-04-02T16:08:03.327Z'),
dateString: '2020-04-02',
boolean: true,
number: 1,
});
expect(yaml.fromFile('# Comment a\na: a\nb:\n # Comment c\n c:\n d: d\n')).toEqual({
a: 'a',
b: { c: { d: 'd' } },
});
expect(
yaml.fromFile(stripIndent`
# template comment
template: post
# title comment
title: title
# image comment
image: /media/netlify.png
# date comment
date: 2020-04-02T13:27:48.617Z
# object comment
object:
# object_title comment
object_title: object_title
# object_list comment
object_list:
- object_list_item_1: "1"
object_list_item_2: "2"
# list comment
list:
- "1"
`),
).toEqual({
list: ['1'],
object: {
object_title: 'object_title',
object_list: [{ object_list_item_1: '1', object_list_item_2: '2' }],
},
date: new Date('2020-04-02T13:27:48.617Z'),
image: '/media/netlify.png',
title: 'title',
template: 'post',
});
});
test('does not fail on closing separator', () => {
expect(yaml.fromFile('---\n[]\n---')).toEqual([]);
});
test('parses single quoted string as string', () => {
expect(yaml.fromFile('name: y')).toEqual({ name: 'y' });
});
test('parses ISO date string as date', () => {
expect(yaml.fromFile('date: 2020-04-02T16:08:03.327Z')).toEqual({
date: new Date('2020-04-02T16:08:03.327Z'),
});
});
test('parses partial date string as string', () => {
expect(yaml.fromFile('date: 2020-06-12')).toEqual({
date: '2020-06-12',
});
expect(yaml.fromFile('date: 12-06-2012')).toEqual({
date: '12-06-2012',
});
});
test('parses partial time value as string', () => {
expect(yaml.fromFile('time: 10:05')).toEqual({
time: '10:05',
});
});
});
describe('toFile', () => {
test('outputs valid yaml', () => {
expect(yaml.toFile([])).toEqual('[]\n');
});
test('should sort keys', () => {
expect(yaml.toFile({ a: 'a', b: 'b', c: 'c', d: 'd' })).toEqual('a: a\nb: b\nc: c\nd: d\n');
expect(yaml.toFile({ a: 'a', b: 'b', c: 'c', d: 'd' }, ['d', 'b', 'a', 'c'])).toEqual(
'd: d\nb: b\na: a\nc: c\n',
);
expect(yaml.toFile({ a: 'a', b: 'b', c: 'c', d: 'd' }, ['d', 'b', 'c'])).toEqual(
'a: a\nd: d\nb: b\nc: c\n',
);
});
test('should add comments', () => {
expect(
yaml.toFile({ a: 'a', b: { c: { d: 'd' } } }, [], { a: 'Comment a', 'b.c': 'Comment c' }),
).toEqual('# Comment a\na: a\nb:\n # Comment c\n c:\n d: d\n');
const expected = `# template comment
template: post
# title comment
title: title
# image comment
image: /media/netlify.png
# date comment
date: 2020-04-02T13:27:48.617Z
# object comment
object:
# object_title comment
object_title: object_title
# object_list comment
object_list:
- object_list_item_1: "1"
object_list_item_2: "2"
# list comment
list:
- "1"
`;
const result = yaml.toFile(
{
list: ['1'],
object: {
object_title: 'object_title',
object_list: [{ object_list_item_1: '1', object_list_item_2: '2' }],
},
date: new Date('2020-04-02T13:27:48.617Z'),
image: '/media/netlify.png',
title: 'title',
template: 'post',
},
['template', 'title', 'image', 'date', 'object', 'list'],
{
list: 'list comment',
object: 'object comment',
'object.object_title': 'object_title comment',
'object.object_list': 'object_list comment',
date: 'date comment',
image: 'image comment',
title: 'title comment',
template: 'template comment',
},
);
expect(result).toEqual(expected);
expect(yaml.toFile({ a: 'a' }, [], { a: 'line 1\\nline 2' })).toEqual(
'# line 1\n# line 2\na: a\n',
);
});
});
});

View File

@ -1,38 +0,0 @@
import { authenticating, authenticate, authError, logout } from '../../actions/auth';
import auth, { defaultState } from '../auth';
describe('auth', () => {
it('should handle an empty state', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore auth reducer doesn't accept empty action
expect(auth(undefined, {})).toEqual(defaultState);
});
it('should handle an authentication request', () => {
expect(auth(undefined, authenticating())).toEqual({
...defaultState,
isFetching: true,
});
});
it('should handle authentication', () => {
const user = { name: 'joe', token: 'token' };
expect(auth(undefined, authenticate(user))).toEqual({
...defaultState,
user,
});
});
it('should handle an authentication error', () => {
expect(auth(undefined, authError(new Error('Bad credentials')))).toEqual({
...defaultState,
error: 'Error: Bad credentials',
});
});
it('should handle logout', () => {
const user = { name: 'joe', token: 'token' };
const newState = auth({ ...defaultState, user }, logout());
expect(newState.user).toBeUndefined();
});
});

View File

@ -1,571 +0,0 @@
import { fromJS, Map } from 'immutable';
import { configLoaded } from '../../actions/config';
import collections, {
selectAllowDeletion,
selectEntryPath,
selectEntrySlug,
selectFieldsWithMediaFolders,
selectMediaFolders,
selectEntryCollectionTitle,
getFieldsNames,
selectField,
updateFieldByKey,
} from '../collections';
import { FILES, FOLDER } from '../../constants/collectionTypes';
describe('collections', () => {
it('should handle an empty state', () => {
expect(collections(undefined, {})).toEqual(Map());
});
it('should load the collections from the config', () => {
expect(
collections(
undefined,
configLoaded({
collections: [
{
name: 'posts',
folder: '_posts',
fields: [{ name: 'title', widget: 'string' }],
},
],
}),
).toJS(),
).toEqual({
posts: {
name: 'posts',
folder: '_posts',
fields: [{ name: 'title', widget: 'string' }],
},
});
});
it('should maintain config collections order', () => {
const collectionsData = new Array(1000).fill(0).map((_, index) => ({
name: `collection_${index}`,
folder: `collection_${index}`,
fields: [{ name: 'title', widget: 'string' }],
}));
const newState = collections(
undefined,
configLoaded({
collections: collectionsData,
}),
);
const keyArray = newState.keySeq().toArray();
expect(keyArray).toEqual(collectionsData.map(({ name }) => name));
});
describe('selectAllowDeletions', () => {
it('should not allow deletions for file collections', () => {
expect(
selectAllowDeletion(
fromJS({
name: 'pages',
type: FILES,
}),
),
).toBe(false);
});
});
describe('selectEntryPath', () => {
it('should return path', () => {
expect(
selectEntryPath(
fromJS({
type: FOLDER,
folder: 'posts',
}),
'dir1/dir2/slug',
),
).toBe('posts/dir1/dir2/slug.md');
});
});
describe('selectEntrySlug', () => {
it('should return slug', () => {
expect(
selectEntrySlug(
fromJS({
type: FOLDER,
folder: 'posts',
}),
'posts/dir1/dir2/slug.md',
),
).toBe('dir1/dir2/slug');
});
});
describe('selectFieldsMediaFolders', () => {
it('should return empty array for invalid collection', () => {
expect(selectFieldsWithMediaFolders(fromJS({}))).toEqual([]);
});
it('should return configs for folder collection', () => {
expect(
selectFieldsWithMediaFolders(
fromJS({
folder: 'posts',
fields: [
{
name: 'image',
media_folder: 'image_media_folder',
},
{
name: 'body',
media_folder: 'body_media_folder',
},
{
name: 'list_1',
field: {
name: 'list_1_item',
media_folder: 'list_1_item_media_folder',
},
},
{
name: 'list_2',
fields: [
{
name: 'list_2_item',
media_folder: 'list_2_item_media_folder',
},
],
},
{
name: 'list_3',
types: [
{
name: 'list_3_type',
media_folder: 'list_3_type_media_folder',
},
],
},
],
}),
),
).toEqual([
fromJS({
name: 'image',
media_folder: 'image_media_folder',
}),
fromJS({ name: 'body', media_folder: 'body_media_folder' }),
fromJS({ name: 'list_1_item', media_folder: 'list_1_item_media_folder' }),
fromJS({
name: 'list_2_item',
media_folder: 'list_2_item_media_folder',
}),
fromJS({
name: 'list_3_type',
media_folder: 'list_3_type_media_folder',
}),
]);
});
it('should return configs for files collection', () => {
expect(
selectFieldsWithMediaFolders(
fromJS({
files: [
{
name: 'file1',
fields: [
{
name: 'image',
media_folder: 'image_media_folder',
},
],
},
{
name: 'file2',
fields: [
{
name: 'body',
media_folder: 'body_media_folder',
},
],
},
{
name: 'file3',
fields: [
{
name: 'list_1',
field: {
name: 'list_1_item',
media_folder: 'list_1_item_media_folder',
},
},
],
},
{
name: 'file4',
fields: [
{
name: 'list_2',
fields: [
{
name: 'list_2_item',
media_folder: 'list_2_item_media_folder',
},
{
name: 'list_3',
types: [
{
name: 'list_3_type',
media_folder: 'list_3_type_media_folder',
},
],
},
],
},
],
},
],
}),
'file4',
),
).toEqual([
fromJS({
name: 'list_2_item',
media_folder: 'list_2_item_media_folder',
}),
fromJS({
name: 'list_3_type',
media_folder: 'list_3_type_media_folder',
}),
]);
});
});
describe('selectMediaFolders', () => {
const slug = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const config = { slug, media_folder: '/static/img' };
it('should return fields and collection folders', () => {
expect(
selectMediaFolders(
config,
fromJS({
folder: 'posts',
media_folder: '{{media_folder}}/general/',
fields: [
{
name: 'image',
media_folder: '{{media_folder}}/customers/',
},
{
name: 'list',
types: [{ name: 'widget', media_folder: '{{media_folder}}/widgets' }],
},
],
}),
fromJS({ slug: 'name', path: 'src/post/post1.md', data: {} }),
),
).toEqual([
'static/img/general',
'static/img/general/customers',
'static/img/general/widgets',
]);
});
it('should return fields, file and collection folders', () => {
expect(
selectMediaFolders(
config,
fromJS({
media_folder: '{{media_folder}}/general/',
files: [
{
name: 'name',
file: 'src/post/post1.md',
media_folder: '{{media_folder}}/customers/',
fields: [
{
name: 'image',
media_folder: '{{media_folder}}/logos/',
},
{
name: 'list',
types: [{ name: 'widget', media_folder: '{{media_folder}}/widgets' }],
},
],
},
],
}),
fromJS({ slug: 'name', path: 'src/post/post1.md', data: {} }),
),
).toEqual([
'static/img/general',
'static/img/general/customers',
'static/img/general/customers/logos',
'static/img/general/customers/widgets',
]);
});
});
describe('getFieldsNames', () => {
it('should get flat fields names', () => {
const collection = fromJS({
fields: [{ name: 'en' }, { name: 'es' }],
});
expect(getFieldsNames(collection.get('fields').toArray())).toEqual(['en', 'es']);
});
it('should get nested fields names', () => {
const collection = fromJS({
fields: [
{ name: 'en', fields: [{ name: 'title' }, { name: 'body' }] },
{ name: 'es', fields: [{ name: 'title' }, { name: 'body' }] },
{ name: 'it', field: { name: 'title', fields: [{ name: 'subTitle' }] } },
{
name: 'fr',
fields: [{ name: 'title', widget: 'list', types: [{ name: 'variableType' }] }],
},
],
});
expect(getFieldsNames(collection.get('fields').toArray())).toEqual([
'en',
'es',
'it',
'fr',
'en.title',
'en.body',
'es.title',
'es.body',
'it.title',
'it.title.subTitle',
'fr.title',
'fr.title.variableType',
]);
});
});
describe('selectField', () => {
it('should return top field by key', () => {
const collection = fromJS({
fields: [{ name: 'en' }, { name: 'es' }],
});
expect(selectField(collection, 'en')).toBe(collection.get('fields').get(0));
});
it('should return nested field by key', () => {
const collection = fromJS({
fields: [
{ name: 'en', fields: [{ name: 'title' }, { name: 'body' }] },
{ name: 'es', fields: [{ name: 'title' }, { name: 'body' }] },
{ name: 'it', field: { name: 'title', fields: [{ name: 'subTitle' }] } },
{
name: 'fr',
fields: [{ name: 'title', widget: 'list', types: [{ name: 'variableType' }] }],
},
],
});
expect(selectField(collection, 'en.title')).toBe(
collection.get('fields').get(0).get('fields').get(0),
);
expect(selectField(collection, 'it.title.subTitle')).toBe(
collection.get('fields').get(2).get('field').get('fields').get(0),
);
expect(selectField(collection, 'fr.title.variableType')).toBe(
collection.get('fields').get(3).get('fields').get(0).get('types').get(0),
);
});
});
describe('selectEntryCollectionTitle', () => {
const entry = fromJS({
data: { title: 'entry title', otherField: 'other field', emptyLinkTitle: '' },
});
it('should return the entry title if set', () => {
const collection = fromJS({
fields: [{ name: 'title' }, { name: 'otherField' }],
});
expect(selectEntryCollectionTitle(collection, entry)).toEqual('entry title');
});
it('should return some other inferreable title if set', () => {
const headlineEntry = fromJS({
data: { headline: 'entry headline', otherField: 'other field' },
});
const collection = fromJS({
fields: [{ name: 'headline' }, { name: 'otherField' }],
});
expect(selectEntryCollectionTitle(collection, headlineEntry)).toEqual('entry headline');
});
it('should return the identifier_field content if defined in collection', () => {
const collection = fromJS({
identifier_field: 'otherField',
fields: [{ name: 'title' }, { name: 'otherField' }],
});
expect(selectEntryCollectionTitle(collection, entry)).toEqual('other field');
});
it('should return the entry title if identifier_field content is not defined in collection', () => {
const collection = fromJS({
identifier_field: 'missingLinkTitle',
fields: [{ name: 'title' }, { name: 'otherField' }],
});
expect(selectEntryCollectionTitle(collection, entry)).toEqual('entry title');
});
it('should return the entry title if identifier_field content is empty', () => {
const collection = fromJS({
identifier_field: 'emptyLinkTitle',
fields: [{ name: 'title' }, { name: 'otherField' }, { name: 'emptyLinkTitle' }],
});
expect(selectEntryCollectionTitle(collection, entry)).toEqual('entry title');
});
it('should return the entry label of a file collection', () => {
const labelEntry = fromJS({
slug: 'entry-name',
data: { title: 'entry title', otherField: 'other field' },
});
const collection = fromJS({
type: FILES,
files: [
{
name: 'entry-name',
label: 'entry label',
},
],
});
expect(selectEntryCollectionTitle(collection, labelEntry)).toEqual('entry label');
});
it('should return a formatted summary before everything else', () => {
const collection = fromJS({
summary: '{{title}} -- {{otherField}}',
identifier_field: 'otherField',
fields: [{ name: 'title' }, { name: 'otherField' }],
});
expect(selectEntryCollectionTitle(collection, entry)).toEqual('entry title -- other field');
});
});
describe('updateFieldByKey', () => {
it('should update field by key', () => {
const collection = fromJS({
fields: [
{ name: 'title' },
{ name: 'image' },
{
name: 'object',
fields: [{ name: 'title' }, { name: 'gallery', fields: [{ name: 'image' }] }],
},
{ name: 'list', field: { name: 'image' } },
{ name: 'body' },
{ name: 'widgetList', types: [{ name: 'widget' }] },
],
});
function updater(field) {
return field.set('default', 'default');
}
expect(updateFieldByKey(collection, 'non-existent', updater)).toBe(collection);
expect(updateFieldByKey(collection, 'title', updater)).toEqual(
fromJS({
fields: [
{ name: 'title', default: 'default' },
{ name: 'image' },
{
name: 'object',
fields: [{ name: 'title' }, { name: 'gallery', fields: [{ name: 'image' }] }],
},
{ name: 'list', field: { name: 'image' } },
{ name: 'body' },
{ name: 'widgetList', types: [{ name: 'widget' }] },
],
}),
);
expect(updateFieldByKey(collection, 'object.title', updater)).toEqual(
fromJS({
fields: [
{ name: 'title' },
{ name: 'image' },
{
name: 'object',
fields: [
{ name: 'title', default: 'default' },
{ name: 'gallery', fields: [{ name: 'image' }] },
],
},
{ name: 'list', field: { name: 'image' } },
{ name: 'body' },
{ name: 'widgetList', types: [{ name: 'widget' }] },
],
}),
);
expect(updateFieldByKey(collection, 'object.gallery.image', updater)).toEqual(
fromJS({
fields: [
{ name: 'title' },
{ name: 'image' },
{
name: 'object',
fields: [
{ name: 'title' },
{ name: 'gallery', fields: [{ name: 'image', default: 'default' }] },
],
},
{ name: 'list', field: { name: 'image' } },
{ name: 'body' },
{ name: 'widgetList', types: [{ name: 'widget' }] },
],
}),
);
expect(updateFieldByKey(collection, 'list.image', updater)).toEqual(
fromJS({
fields: [
{ name: 'title' },
{ name: 'image' },
{
name: 'object',
fields: [{ name: 'title' }, { name: 'gallery', fields: [{ name: 'image' }] }],
},
{ name: 'list', field: { name: 'image', default: 'default' } },
{ name: 'body' },
{ name: 'widgetList', types: [{ name: 'widget' }] },
],
}),
);
expect(updateFieldByKey(collection, 'widgetList.widget', updater)).toEqual(
fromJS({
fields: [
{ name: 'title' },
{ name: 'image' },
{
name: 'object',
fields: [{ name: 'title' }, { name: 'gallery', fields: [{ name: 'image' }] }],
},
{ name: 'list', field: { name: 'image' } },
{ name: 'body' },
{ name: 'widgetList', types: [{ name: 'widget', default: 'default' }] },
],
}),
);
});
});
});

View File

@ -1,38 +0,0 @@
import { configLoaded, configLoading, configFailed } from '../../actions/config';
import config, { selectLocale } from '../config';
describe('config', () => {
it('should handle an empty state', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore config reducer doesn't accept empty action
expect(config(undefined, {})).toEqual({ isFetching: true });
});
it('should handle an update', () => {
expect(
config({ isFetching: true }, configLoaded({ locale: 'fr', backend: { name: 'proxy' } })),
).toEqual({
locale: 'fr',
backend: { name: 'proxy' },
isFetching: false,
error: undefined,
});
});
it('should mark the config as loading', () => {
expect(config({ isFetching: false }, configLoading())).toEqual({ isFetching: true });
});
it('should handle an error', () => {
expect(
config({ isFetching: true }, configFailed(new Error('Config could not be loaded'))),
).toEqual({
error: 'Error: Config could not be loaded',
isFetching: false,
});
});
it('should default to "en" locale', () => {
expect(selectLocale({})).toEqual('en');
});
});

View File

@ -1,694 +0,0 @@
import { OrderedMap, fromJS } from 'immutable';
import * as actions from '../../actions/entries';
import reducer, {
selectMediaFolder,
selectMediaFilePath,
selectMediaFilePublicPath,
selectEntries,
} from '../entries';
const initialState = OrderedMap({
posts: fromJS({ name: 'posts' }),
});
describe('entries', () => {
describe('reducer', () => {
it('should mark entries as fetching', () => {
expect(reducer(initialState, actions.entriesLoading(fromJS({ name: 'posts' })))).toEqual(
OrderedMap(
fromJS({
posts: { name: 'posts' },
pages: {
posts: { isFetching: true },
},
}),
),
);
});
it('should handle loaded entries', () => {
const entries = [
{ slug: 'a', path: '' },
{ slug: 'b', title: 'B' },
];
expect(
reducer(initialState, actions.entriesLoaded(fromJS({ name: 'posts' }), entries, 0)),
).toEqual(
OrderedMap(
fromJS({
posts: { name: 'posts' },
entities: {
'posts.a': { slug: 'a', path: '', isFetching: false },
'posts.b': { slug: 'b', title: 'B', isFetching: false },
},
pages: {
posts: {
page: 0,
ids: ['a', 'b'],
},
},
}),
),
);
});
it('should handle loaded entry', () => {
const entry = { slug: 'a', path: '' };
expect(reducer(initialState, actions.entryLoaded(fromJS({ name: 'posts' }), entry))).toEqual(
OrderedMap(
fromJS({
posts: { name: 'posts' },
entities: {
'posts.a': { slug: 'a', path: '' },
},
pages: {
posts: {
ids: ['a'],
},
},
}),
),
);
});
});
describe('selectMediaFolder', () => {
it("should return global media folder when collection doesn't specify media_folder", () => {
expect(
selectMediaFolder(
{ media_folder: 'static/media' },
fromJS({ name: 'posts' }),
undefined,
undefined,
),
).toEqual('static/media');
});
it('should return draft media folder when collection specifies media_folder and entry is undefined', () => {
expect(
selectMediaFolder(
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
undefined,
undefined,
),
).toEqual('posts/DRAFT_MEDIA_FILES');
});
it('should return relative media folder when collection specifies media_folder and entry path is not null', () => {
expect(
selectMediaFolder(
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
fromJS({ path: 'posts/title/index.md' }),
undefined,
),
).toEqual('posts/title');
});
it('should resolve collection relative media folder', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media' }),
fromJS({ name: 'posts', folder: 'posts', media_folder: '../' }),
fromJS({ path: 'posts/title/index.md' }),
undefined,
),
).toEqual('posts');
});
it('should resolve field relative media folder', () => {
const field = fromJS({ media_folder: '' });
expect(
selectMediaFolder(
{ media_folder: '/static/img' },
fromJS({
name: 'other',
folder: 'other',
fields: [field],
media_folder: '../',
}),
fromJS({ path: 'src/other/other.md', data: {} }),
field,
),
).toEqual('src/other');
});
it('should return collection absolute media folder without leading slash', () => {
expect(
selectMediaFolder(
{ media_folder: '/static/Images' },
fromJS({
name: 'getting-started',
folder: 'src/docs/getting-started',
media_folder: '/static/images/docs/getting-started',
}),
fromJS({}),
undefined,
),
).toEqual('static/images/docs/getting-started');
});
it('should compile relative media folder template', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const collection = fromJS({
name: 'posts',
folder: 'content',
media_folder: '../../../{{media_folder}}/{{category}}/{{slug}}',
fields: [{ name: 'title', widget: 'string' }],
});
expect(
selectMediaFolder(
{ media_folder: 'static/media', slug: slugConfig },
collection,
entry,
undefined,
),
).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox');
});
it('should compile absolute media folder template', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
data: { title: 'Overview' },
});
const collection = fromJS({
name: 'extending',
folder: 'src/docs/extending',
media_folder: '{{media_folder}}/docs/extending',
fields: [{ name: 'title', widget: 'string' }],
});
expect(
selectMediaFolder(
{ media_folder: '/static/images', slug: slugConfig },
collection,
entry,
undefined,
),
).toEqual('static/images/docs/extending');
});
it('should compile field media folder template', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const collection = fromJS({
name: 'posts',
folder: 'content',
fields: [
{
name: 'title',
widget: 'string',
media_folder: '../../../{{media_folder}}/{{category}}/{{slug}}',
},
],
});
expect(
selectMediaFolder(
{ media_folder: 'static/media', slug: slugConfig },
collection,
entry,
collection.get('fields').get(0),
),
).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox');
});
it('should handle double slashes', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const collection = fromJS({
name: 'posts',
folder: 'content',
media_folder: '{{media_folder}}/blog',
fields: [{ name: 'title', widget: 'string' }],
});
expect(
selectMediaFolder(
{ media_folder: '/static/img/', slug: slugConfig },
collection,
entry,
undefined,
),
).toEqual('static/img/blog');
expect(
selectMediaFolder(
{ media_folder: 'static/img/', slug: slugConfig },
collection,
entry,
undefined,
),
).toEqual('content/en/hosting-and-deployment/static/img/blog');
});
it('should handle file media_folder', () => {
expect(
selectMediaFolder(
{ media_folder: 'static/media' },
fromJS({ name: 'posts', files: [{ name: 'index', media_folder: '/static/images/' }] }),
fromJS({ path: 'posts/title/index.md', slug: 'index' }),
undefined,
),
).toBe('static/images');
});
it('should cascade media_folders', () => {
const mainImageField = fromJS({ name: 'main_image' });
const logoField = fromJS({ name: 'logo', media_folder: '{{media_folder}}/logos/' });
const nestedField3 = fromJS({ name: 'nested', media_folder: '{{media_folder}}/nested3/' });
const nestedField2 = fromJS({
name: 'nested',
media_folder: '{{media_folder}}/nested2/',
types: [nestedField3],
});
const nestedField1 = fromJS({
name: 'nested',
media_folder: '{{media_folder}}/nested1/',
fields: [nestedField2],
});
const args = [
{ media_folder: '/static/img' },
fromJS({
name: 'general',
media_folder: '{{media_folder}}/general/',
files: [
{
name: 'customers',
media_folder: '{{media_folder}}/customers/',
fields: [
mainImageField,
logoField,
{ media_folder: '{{media_folder}}/nested', field: nestedField1 },
],
},
],
}),
fromJS({ path: 'src/customers/customers.md', slug: 'customers', data: { title: 'title' } }),
];
expect(selectMediaFolder(...args, mainImageField)).toBe('static/img/general/customers');
expect(selectMediaFolder(...args, logoField)).toBe('static/img/general/customers/logos');
expect(selectMediaFolder(...args, nestedField1)).toBe(
'static/img/general/customers/nested/nested1',
);
expect(selectMediaFolder(...args, nestedField2)).toBe(
'static/img/general/customers/nested/nested1/nested2',
);
expect(selectMediaFolder(...args, nestedField3)).toBe(
'static/img/general/customers/nested/nested1/nested2/nested3',
);
});
});
describe('selectMediaFilePath', () => {
it('should return absolute URL as is', () => {
expect(selectMediaFilePath(null, null, null, 'https://www.netlify.com/image.png')).toBe(
'https://www.netlify.com/image.png',
);
});
it('should resolve path from global media folder for collection with no media folder', () => {
expect(
selectMediaFilePath(
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts' }),
undefined,
'image.png',
undefined,
),
).toBe('static/media/image.png');
});
it('should resolve path from collection media folder for collection with media folder', () => {
expect(
selectMediaFilePath(
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
undefined,
'image.png',
undefined,
),
).toBe('posts/DRAFT_MEDIA_FILES/image.png');
});
it('should handle relative media_folder', () => {
expect(
selectMediaFilePath(
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
fromJS({ path: 'posts/title/index.md' }),
'image.png',
undefined,
),
).toBe('static/media/image.png');
});
it('should handle field media_folder', () => {
const field = fromJS({ media_folder: '../../static/media/' });
expect(
selectMediaFilePath(
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', fields: [field] }),
fromJS({ path: 'posts/title/index.md' }),
'image.png',
field,
),
).toBe('static/media/image.png');
});
});
describe('selectMediaFilePublicPath', () => {
it('should return absolute URL as is', () => {
expect(selectMediaFilePublicPath(null, null, 'https://www.netlify.com/image.png')).toBe(
'https://www.netlify.com/image.png',
);
});
it('should resolve path from public folder for collection with no media folder', () => {
expect(
selectMediaFilePublicPath(
{ public_folder: '/media' },
null,
'/media/image.png',
undefined,
undefined,
),
).toBe('/media/image.png');
});
it('should resolve path from collection public folder for collection with public folder', () => {
expect(
selectMediaFilePublicPath(
{ public_folder: '/media' },
fromJS({ name: 'posts', folder: 'posts', public_folder: '' }),
'image.png',
undefined,
undefined,
),
).toBe('image.png');
});
it('should handle relative public_folder', () => {
expect(
selectMediaFilePublicPath(
{ public_folder: '/media' },
fromJS({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }),
'image.png',
undefined,
undefined,
),
).toBe('../../static/media/image.png');
});
it('should handle absolute public_folder', () => {
expect(
selectMediaFilePublicPath(
{ public_folder: 'https://www.netlify.com/media' },
fromJS({
name: 'posts',
folder: 'posts',
public_folder: 'https://www.netlify.com/media',
}),
'image.png',
undefined,
undefined,
),
).toBe('https://www.netlify.com/media/image.png');
});
it('should compile collection public folder template', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const collection = fromJS({
name: 'posts',
folder: 'content',
public_folder: '/{{public_folder}}/{{category}}/{{slug}}',
fields: [{ name: 'title', widget: 'string' }],
});
expect(
selectMediaFilePublicPath(
{ public_folder: 'static/media', slug: slugConfig },
collection,
'image.png',
entry,
undefined,
),
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
});
it('should compile field public folder template', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const field = fromJS({
name: 'title',
widget: 'string',
public_folder: '/{{public_folder}}/{{category}}/{{slug}}',
});
const collection = fromJS({
name: 'posts',
folder: 'content',
fields: [field],
});
expect(
selectMediaFilePublicPath(
{ public_folder: 'static/media', slug: slugConfig },
collection,
'image.png',
entry,
field,
),
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
});
it('should handle double slashes', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const field = fromJS({
name: 'title',
widget: 'string',
public_folder: '/{{public_folder}}/{{category}}/{{slug}}',
});
const collection = fromJS({
name: 'posts',
folder: 'content',
fields: [field],
});
expect(
selectMediaFilePublicPath(
{ public_folder: 'static/media/', slug: slugConfig },
collection,
'image.png',
entry,
field,
),
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
});
it('should handle file public_folder', () => {
const entry = fromJS({
path: 'src/posts/index.md',
slug: 'index',
});
const collection = fromJS({
name: 'posts',
files: [
{
name: 'index',
public_folder: '/images',
fields: [{ name: 'title', widget: 'string' }],
},
],
});
expect(
selectMediaFilePublicPath(
{ public_folder: 'static/media/' },
collection,
'image.png',
entry,
undefined,
),
).toBe('/images/image.png');
});
});
describe('selectEntries', () => {
it('should return all entries', () => {
const state = fromJS({
entities: {
'posts.1': { slug: '1' },
'posts.2': { slug: '2' },
'posts.3': { slug: '3' },
'posts.4': { slug: '4' },
},
pages: { posts: { ids: ['1', '2', '3', '4'] } },
});
const collection = fromJS({
name: 'posts',
});
expect(selectEntries(state, collection)).toEqual(
fromJS([{ slug: '1' }, { slug: '2' }, { slug: '3' }, { slug: '4' }]),
);
});
});
it('should return sorted entries entries by field', () => {
const state = fromJS({
entities: {
'posts.1': { slug: '1', data: { title: '1' } },
'posts.2': { slug: '2', data: { title: '2' } },
'posts.3': { slug: '3', data: { title: '3' } },
'posts.4': { slug: '4', data: { title: '4' } },
},
pages: { posts: { ids: ['1', '2', '3', '4'] } },
sort: { posts: { title: { key: 'title', direction: 'Descending' } } },
});
const collection = fromJS({
name: 'posts',
});
expect(selectEntries(state, collection)).toEqual(
fromJS([
{ slug: '4', data: { title: '4' } },
{ slug: '3', data: { title: '3' } },
{ slug: '2', data: { title: '2' } },
{ slug: '1', data: { title: '1' } },
]),
);
});
it('should return sorted entries entries by nested field', () => {
const state = fromJS({
entities: {
'posts.1': { slug: '1', data: { title: '1', nested: { date: 4 } } },
'posts.2': { slug: '2', data: { title: '2', nested: { date: 3 } } },
'posts.3': { slug: '3', data: { title: '3', nested: { date: 2 } } },
'posts.4': { slug: '4', data: { title: '4', nested: { date: 1 } } },
},
pages: { posts: { ids: ['1', '2', '3', '4'] } },
sort: { posts: { title: { key: 'nested.date', direction: 'Ascending' } } },
});
const collection = fromJS({
name: 'posts',
});
expect(selectEntries(state, collection)).toEqual(
fromJS([
{ slug: '4', data: { title: '4', nested: { date: 1 } } },
{ slug: '3', data: { title: '3', nested: { date: 2 } } },
{ slug: '2', data: { title: '2', nested: { date: 3 } } },
{ slug: '1', data: { title: '1', nested: { date: 4 } } },
]),
);
});
it('should return filtered entries entries by field', () => {
const state = fromJS({
entities: {
'posts.1': { slug: '1', data: { title: '1' } },
'posts.2': { slug: '2', data: { title: '2' } },
'posts.3': { slug: '3', data: { title: '3' } },
'posts.4': { slug: '4', data: { title: '4' } },
},
pages: { posts: { ids: ['1', '2', '3', '4'] } },
filter: { posts: { title__1: { field: 'title', pattern: '4', active: true } } },
});
const collection = fromJS({
name: 'posts',
});
expect(selectEntries(state, collection)).toEqual(fromJS([{ slug: '4', data: { title: '4' } }]));
});
it('should return filtered entries entries by nested field', () => {
const state = fromJS({
entities: {
'posts.1': { slug: '1', data: { title: '1', nested: { draft: true } } },
'posts.2': { slug: '2', data: { title: '2', nested: { draft: true } } },
'posts.3': { slug: '3', data: { title: '3', nested: { draft: false } } },
'posts.4': { slug: '4', data: { title: '4', nested: { draft: false } } },
},
pages: { posts: { ids: ['1', '2', '3', '4'] } },
filter: {
posts: { 'nested.draft__false': { field: 'nested.draft', pattern: false, active: true } },
},
});
const collection = fromJS({
name: 'posts',
});
expect(selectEntries(state, collection)).toEqual(
fromJS([
{ slug: '3', data: { title: '3', nested: { draft: false } } },
{ slug: '4', data: { title: '4', nested: { draft: false } } },
]),
);
});
});

View File

@ -1,198 +0,0 @@
import { Map, fromJS } from 'immutable';
import * as actions from '../../actions/entries';
import reducer from '../entryDraft';
jest.mock('uuid/v4', () => jest.fn(() => '1'));
const initialState = Map({
entry: Map(),
fieldsMetaData: Map(),
fieldsErrors: Map(),
hasChanged: false,
key: '',
});
const entry = {
collection: 'posts',
slug: 'slug',
path: 'content/blog/art-and-wine-festival.md',
partial: false,
raw: '',
data: {},
metaData: null,
};
describe('entryDraft reducer', () => {
describe('DRAFT_CREATE_FROM_ENTRY', () => {
it('should create draft from the entry', () => {
const state = reducer(initialState, actions.createDraftFromEntry(fromJS(entry)));
expect(state).toEqual(
fromJS({
entry: {
...entry,
newRecord: false,
},
fieldsMetaData: Map(),
fieldsErrors: Map(),
hasChanged: false,
key: '1',
}),
);
});
});
describe('DRAFT_CREATE_EMPTY', () => {
it('should create a new draft ', () => {
const state = reducer(initialState, actions.emptyDraftCreated(fromJS(entry)));
expect(state).toEqual(
fromJS({
entry: {
...entry,
newRecord: true,
},
fieldsMetaData: Map(),
fieldsErrors: Map(),
hasChanged: false,
key: '1',
}),
);
});
});
describe('DRAFT_DISCARD', () => {
it('should discard the draft and return initial state', () => {
expect(reducer(initialState, actions.discardDraft())).toEqual(initialState);
});
});
describe('persisting', () => {
let initialState;
beforeEach(() => {
initialState = fromJS({
entities: {
'posts.slug': {
collection: 'posts',
slug: 'slug',
path: 'content/blog/art-and-wine-festival.md',
partial: false,
raw: '',
data: {},
metaData: null,
},
},
pages: {},
});
});
it('should handle persisting request', () => {
const newState = reducer(
initialState,
actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })),
);
expect(newState.getIn(['entry', 'isPersisting'])).toBe(true);
});
it('should handle persisting success', () => {
let newState = reducer(
initialState,
actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })),
);
newState = reducer(
newState,
actions.entryPersisted(Map({ name: 'posts' }), Map({ slug: 'slug' })),
);
expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined();
});
it('should handle persisting error', () => {
let newState = reducer(
initialState,
actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })),
);
newState = reducer(
newState,
actions.entryPersistFail(Map({ name: 'posts' }), Map({ slug: 'slug' }), 'Error message'),
);
expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined();
});
});
describe('REMOVE_DRAFT_ENTRY_MEDIA_FILE', () => {
it('should remove a media file', () => {
const actualState = reducer(
initialState.setIn(['entry', 'mediaFiles'], fromJS([{ id: '1' }, { id: '2' }])),
actions.removeDraftEntryMediaFile({ id: '1' }),
);
expect(actualState.toJS()).toEqual({
entry: { mediaFiles: [{ id: '2' }] },
fieldsMetaData: {},
fieldsErrors: {},
hasChanged: true,
key: '',
});
});
});
describe('ADD_DRAFT_ENTRY_MEDIA_FILE', () => {
it('should overwrite an existing media file', () => {
const actualState = reducer(
initialState.setIn(['entry', 'mediaFiles'], fromJS([{ id: '1', name: 'old' }])),
actions.addDraftEntryMediaFile({ id: '1', name: 'new' }),
);
expect(actualState.toJS()).toEqual({
entry: { mediaFiles: [{ id: '1', name: 'new' }] },
fieldsMetaData: {},
fieldsErrors: {},
hasChanged: true,
key: '',
});
});
});
describe('DRAFT_CREATE_FROM_LOCAL_BACKUP', () => {
it('should create draft from local backup', () => {
const localBackup = Map({ entry: fromJS({ ...entry, mediaFiles: [{ id: '1' }] }) });
const actualState = reducer(initialState.set('localBackup', localBackup), {
type: actions.DRAFT_CREATE_FROM_LOCAL_BACKUP,
});
expect(actualState.toJS()).toEqual({
entry: {
...entry,
mediaFiles: [{ id: '1' }],
newRecord: false,
},
fieldsMetaData: {},
fieldsErrors: {},
hasChanged: true,
key: '1',
});
});
});
describe('DRAFT_LOCAL_BACKUP_RETRIEVED', () => {
it('should set local backup', () => {
const mediaFiles = [{ id: '1' }];
const actualState = reducer(
initialState,
actions.localBackupRetrieved({ ...entry, mediaFiles }),
);
expect(actualState.toJS()).toEqual({
entry: {},
fieldsMetaData: {},
fieldsErrors: {},
hasChanged: false,
localBackup: {
entry: { ...entry, mediaFiles: [{ id: '1' }] },
},
key: '',
});
});
});
});

View File

@ -1,43 +0,0 @@
import { USE_OPEN_AUTHORING } from '../../actions/auth';
import {
DEPLOY_PREVIEW_REQUEST,
DEPLOY_PREVIEW_SUCCESS,
DEPLOY_PREVIEW_FAILURE,
} from '../../actions/deploys';
import { ENTRY_REQUEST, ENTRY_SUCCESS, ENTRY_FAILURE } from '../../actions/entries';
import reducer from '../globalUI';
describe('globalUI', () => {
it('should set isFetching to true on entry request', () => {
expect(reducer({ isFetching: false }, { type: ENTRY_REQUEST })).toEqual({ isFetching: true });
});
it('should set isFetching to false on entry success', () => {
expect(reducer({ isFetching: true }, { type: ENTRY_SUCCESS })).toEqual({ isFetching: false });
});
it('should set isFetching to false on entry failure', () => {
expect(reducer({ isFetching: true }, { type: ENTRY_FAILURE })).toEqual({ isFetching: false });
});
it('should not change state on deploy preview request', () => {
const state = { isFetching: false };
expect(reducer(state, { type: DEPLOY_PREVIEW_REQUEST })).toBe(state);
});
it('should not change state on deploy preview success', () => {
const state = { isFetching: true };
expect(reducer(state, { type: DEPLOY_PREVIEW_SUCCESS })).toBe(state);
});
it('should not change state on deploy preview failure', () => {
const state = { isFetching: true };
expect(reducer(state, { type: DEPLOY_PREVIEW_FAILURE })).toBe(state);
});
it('should set useOpenAuthoring to true on USE_OPEN_AUTHORING', () => {
expect(reducer({ useOpenAuthoring: false }, { type: USE_OPEN_AUTHORING })).toEqual({
useOpenAuthoring: true,
});
});
});

View File

@ -1,76 +0,0 @@
import integrations from '../integrations';
import { CONFIG_SUCCESS } from '../../actions/config';
import { FOLDER } from '../../constants/collectionTypes';
import type { ConfigAction } from '../../actions/config';
describe('integrations', () => {
it('should return default state when no integrations', () => {
const result = integrations(null, {
type: CONFIG_SUCCESS,
payload: { integrations: [] },
} as ConfigAction);
expect(result && result.toJS()).toEqual({
providers: {},
hooks: {},
});
});
it('should return hooks and providers map when has integrations', () => {
const result = integrations(null, {
type: CONFIG_SUCCESS,
payload: {
integrations: [
{
hooks: ['listEntries'],
collections: '*',
provider: 'algolia',
applicationID: 'applicationID',
apiKey: 'apiKey',
},
{
hooks: ['listEntries'],
collections: ['posts'],
provider: 'algolia',
applicationID: 'applicationID',
apiKey: 'apiKey',
},
{
hooks: ['assetStore'],
provider: 'assetStore',
getSignedFormURL: 'https://asset.store.com/signedUrl',
},
],
collections: [
{ name: 'posts', label: 'Posts', type: FOLDER },
{ name: 'pages', label: 'Pages', type: FOLDER },
{ name: 'faq', label: 'FAQ', type: FOLDER },
],
},
} as ConfigAction);
expect(result && result.toJS()).toEqual({
providers: {
algolia: {
applicationID: 'applicationID',
apiKey: 'apiKey',
},
assetStore: {
getSignedFormURL: 'https://asset.store.com/signedUrl',
},
},
hooks: {
posts: {
listEntries: 'algolia',
},
pages: {
listEntries: 'algolia',
},
faq: {
listEntries: 'algolia',
},
assetStore: 'assetStore',
},
});
});
});

View File

@ -1,154 +0,0 @@
import { Map, fromJS } from 'immutable';
import { mediaDeleted } from '../../actions/mediaLibrary';
import mediaLibrary, {
selectMediaFiles,
selectMediaFileByPath,
selectMediaDisplayURL,
} from '../mediaLibrary';
jest.mock('uuid/v4');
jest.mock('../entries');
jest.mock('../');
describe('mediaLibrary', () => {
it('should remove media file by key', () => {
expect(
mediaLibrary(
Map({
files: [{ key: 'key1' }, { key: 'key2' }],
}),
mediaDeleted({ key: 'key1' }),
),
).toEqual(
Map({
isDeleting: false,
files: [{ key: 'key2' }],
}),
);
});
it('should remove media file by id', () => {
expect(
mediaLibrary(
Map({
files: [{ id: 'id1' }, { id: 'id2' }],
}),
mediaDeleted({ id: 'id1' }),
),
).toEqual(
Map({
isDeleting: false,
files: [{ id: 'id2' }],
}),
);
});
it('should select draft media files from field when editing a draft', () => {
const { selectEditingDraft, selectMediaFolder } = require('../../reducers/entries');
selectEditingDraft.mockReturnValue(true);
selectMediaFolder.mockReturnValue('/static/images/posts/logos');
const imageField = fromJS({ name: 'image' });
const collection = fromJS({ fields: [imageField] });
const entry = fromJS({
collection: 'posts',
mediaFiles: [
{ id: 1, path: '/static/images/posts/logos/logo.png' },
{ id: 2, path: '/static/images/posts/general/image.png' },
{ id: 3, path: '/static/images/posts/index.png' },
],
data: {},
});
const state = {
config: {},
collections: fromJS({ posts: collection }),
entryDraft: fromJS({
entry,
}),
};
expect(selectMediaFiles(state, imageField)).toEqual([
{ id: 1, key: 1, path: '/static/images/posts/logos/logo.png' },
]);
expect(selectMediaFolder).toHaveBeenCalledWith(state.config, collection, entry, imageField);
});
it('should select draft media files from collection when editing a draft', () => {
const { selectEditingDraft, selectMediaFolder } = require('../../reducers/entries');
selectEditingDraft.mockReturnValue(true);
selectMediaFolder.mockReturnValue('/static/images/posts');
const imageField = fromJS({ name: 'image' });
const collection = fromJS({ fields: [imageField] });
const entry = fromJS({
collection: 'posts',
mediaFiles: [
{ id: 1, path: '/static/images/posts/logos/logo.png' },
{ id: 2, path: '/static/images/posts/general/image.png' },
{ id: 3, path: '/static/images/posts/index.png' },
],
data: {},
});
const state = {
config: {},
collections: fromJS({ posts: collection }),
entryDraft: fromJS({
entry,
}),
};
expect(selectMediaFiles(state, imageField)).toEqual([
{ id: 3, key: 3, path: '/static/images/posts/index.png' },
]);
expect(selectMediaFolder).toHaveBeenCalledWith(state.config, collection, entry, imageField);
});
it('should select global media files when not editing a draft', () => {
const { selectEditingDraft } = require('../../reducers/entries');
selectEditingDraft.mockReturnValue(false);
const state = {
mediaLibrary: Map({ files: [{ id: 1 }] }),
};
expect(selectMediaFiles(state)).toEqual([{ id: 1 }]);
});
it('should select global media files when not using asset store integration', () => {
const { selectIntegration } = require('../../reducers');
selectIntegration.mockReturnValue({});
const state = {
mediaLibrary: Map({ files: [{ id: 1 }] }),
};
expect(selectMediaFiles(state)).toEqual([{ id: 1 }]);
});
it('should return media file by path', () => {
const { selectEditingDraft } = require('../../reducers/entries');
selectEditingDraft.mockReturnValue(false);
const state = {
mediaLibrary: Map({ files: [{ id: 1, path: 'path' }] }),
};
expect(selectMediaFileByPath(state, 'path')).toEqual({ id: 1, path: 'path' });
});
it('should return media display URL state', () => {
const state = {
mediaLibrary: fromJS({ displayURLs: { id: { url: 'url' } } }),
};
expect(selectMediaDisplayURL(state, 'id')).toEqual(Map({ url: 'url' }));
});
});

View File

@ -1,49 +0,0 @@
import {
addAssets,
addAsset,
removeAsset,
loadAssetRequest,
loadAssetSuccess,
loadAssetFailure,
} from '../../actions/media';
import reducer from '../medias';
import { createAssetProxy } from '../../valueObjects/AssetProxy';
describe('medias', () => {
const asset = createAssetProxy({ url: 'url', path: 'path' });
it('should add assets', () => {
expect(reducer({}, addAssets([asset]))).toEqual({
path: { asset, isLoading: false, error: null },
});
});
it('should add asset', () => {
expect(reducer({}, addAsset(asset))).toEqual({
path: { asset, isLoading: false, error: null },
});
});
it('should remove asset', () => {
expect(
reducer({ [asset.path]: { asset, isLoading: false, error: null } }, removeAsset(asset.path)),
).toEqual({});
});
it('should mark asset as loading', () => {
expect(reducer({}, loadAssetRequest(asset.path))).toEqual({ path: { isLoading: true } });
});
it('should mark asset as not loading', () => {
expect(reducer({}, loadAssetSuccess(asset.path))).toEqual({
path: { isLoading: false, error: null },
});
});
it('should set loading error', () => {
const error = new Error('some error');
expect(reducer({}, loadAssetFailure(asset.path, error))).toEqual({
path: { isLoading: false, error },
});
});
});

View File

@ -1,49 +0,0 @@
import { createHashHistory } from 'history';
import { mocked } from 'ts-jest/utils';
import type { History } from 'history';
jest.mock('history');
const history = { push: jest.fn(), replace: jest.fn() } as unknown as History;
const mockedCreateHashHistory = mocked(createHashHistory);
mockedCreateHashHistory.mockReturnValue(history);
describe('history', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('navigateToCollection', () => {
it('should push route', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { navigateToCollection } = require('../history');
navigateToCollection('posts');
expect(history.push).toHaveBeenCalledTimes(1);
expect(history.push).toHaveBeenCalledWith('/collections/posts');
});
});
describe('navigateToNewEntry', () => {
it('should replace route', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { navigateToNewEntry } = require('../history');
navigateToNewEntry('posts');
expect(history.replace).toHaveBeenCalledTimes(1);
expect(history.replace).toHaveBeenCalledWith('/collections/posts/new');
});
});
describe('navigateToEntry', () => {
it('should replace route', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { navigateToEntry } = require('../history');
navigateToEntry('posts', 'index');
expect(history.replace).toHaveBeenCalledTimes(1);
expect(history.replace).toHaveBeenCalledWith('/collections/posts/entries/index');
});
});
});

View File

@ -1,129 +0,0 @@
const webpack = require('webpack');
const path = require('path');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const { flatMap } = require('lodash');
const isProduction = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';
const pkg = require('./package.json');
const devServerPort = parseInt(process.env.SIMPLE_CMS_DEV_SERVER_PORT || `${8080}`);
function moduleNameToPath(libName) {
return path.resolve(__dirname, 'node_modules', libName);
}
function rules() {
return {
js: () => ({
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
rootMode: 'upward',
},
},
}),
css: () => [
{
test: /\.css$/,
include: ['ol', 'react-datetime', 'codemirror'].map(moduleNameToPath),
use: ['to-string-loader', 'css-loader'],
},
],
svg: () => ({
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
exclude: [/node_modules/],
use: [
{
loader: 'babel-loader',
options: {
rootMode: 'upward',
},
},
{
loader: 'react-svg-loader',
options: {
jsx: true, // true outputs JSX tags
},
},
],
}),
};
}
function plugins() {
return {
ignoreEsprima: () => new webpack.IgnorePlugin(/^esprima$/, /js-yaml/),
ignoreMomentOptionalDeps: () => new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
};
}
function stats() {
if (isProduction) {
return {
builtAt: false,
chunks: false,
colors: true,
entrypoints: false,
errorDetails: false,
hash: false,
modules: false,
timings: false,
version: false,
warnings: false,
};
}
return {
all: false,
};
}
module.exports = {
context: process.cwd(),
mode: isProduction ? 'production' : 'development',
entry: './src',
output: {
path: path.resolve(process.cwd(), 'dist'),
filename: `simple-cms-core.js`,
library: 'SimpleCmsCore',
libraryTarget: 'umd',
libraryExport: 'SimpleCmsCore',
umdNamedDefine: true,
globalObject: 'window',
},
module: {
rules: flatMap(Object.values(rules()), rule => rule()),
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
alias: {
moment$: 'moment/moment.js',
'react-dom': '@hot-loader/react-dom',
},
},
devtool: isTest ? '' : 'source-map',
target: 'web',
stats: stats(),
plugins: [
...Object.values(plugins()).map(plugin => plugin()),
new webpack.DefinePlugin({
SIMPLE_CMS_CORE_VERSION: JSON.stringify(`${pkg.version}${isProduction ? '' : '-dev'}`),
}),
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [`Simple CMS is now running at http://localhost:${devServerPort}`],
},
}),
],
devServer: {
contentBase: './dev-test',
watchContentBase: true,
publicPath: './dist',
quiet: true,
host: '0.0.0.0',
port: devServerPort,
},
};

3423
yarn.lock

File diff suppressed because it is too large Load Diff