feat: v4.0.0 (#1016)
Co-authored-by: Denys Konovalov <kontakt@denyskon.de> Co-authored-by: Mathieu COSYNS <64072917+Mathieu-COSYNS@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
682576ffc4
commit
799c7e6936
@ -1,10 +1,20 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import type { BaseField, Collection, Field } from '@staticcms/core/interface';
|
||||
import type {
|
||||
BaseField,
|
||||
Collection,
|
||||
CollectionFile,
|
||||
CollectionFileWithDefaults,
|
||||
CollectionWithDefaults,
|
||||
Field,
|
||||
FilesCollection,
|
||||
FilesCollectionWithDefaults,
|
||||
FolderCollection,
|
||||
FolderCollectionWithDefaults,
|
||||
} from '@staticcms/core';
|
||||
|
||||
export const createMockCollection = <EF extends BaseField>(
|
||||
export const createMockFolderCollection = <EF extends BaseField>(
|
||||
extra: Partial<Collection<EF>> = {},
|
||||
...fields: Field<EF>[]
|
||||
): Collection<EF> => ({
|
||||
): FolderCollection<EF> => ({
|
||||
name: 'mock_collection',
|
||||
label: 'Mock Collections',
|
||||
label_singular: 'Mock Collection',
|
||||
@ -29,3 +39,67 @@ export const createMockCollection = <EF extends BaseField>(
|
||||
],
|
||||
...extra,
|
||||
});
|
||||
|
||||
export const createMockFolderCollectionWithDefaults = <EF extends BaseField>(
|
||||
extra: Partial<CollectionWithDefaults<EF>> = {},
|
||||
...fields: Field<EF>[]
|
||||
): FolderCollectionWithDefaults<EF> => ({
|
||||
...createMockFolderCollection(extra, ...fields),
|
||||
i18n: extra.i18n,
|
||||
});
|
||||
|
||||
export const createMockCollectionFile = <EF extends BaseField>(
|
||||
extra: Partial<CollectionFile<EF>> = {},
|
||||
...fields: Field<EF>[]
|
||||
): CollectionFile<EF> => ({
|
||||
name: 'mock_collection',
|
||||
label: 'Mock Collections',
|
||||
label_singular: 'Mock Collection',
|
||||
file: 'mock_collection.md',
|
||||
description:
|
||||
'The description is a great place for tone setting, high level information, and editing guidelines that are specific to a collection.\n',
|
||||
fields: [
|
||||
{
|
||||
label: 'Title',
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
},
|
||||
...fields,
|
||||
],
|
||||
...extra,
|
||||
});
|
||||
|
||||
export const createMockCollectionFileWithDefaults = <EF extends BaseField>(
|
||||
extra: Partial<CollectionFileWithDefaults<EF>> = {},
|
||||
...fields: Field<EF>[]
|
||||
): CollectionFileWithDefaults<EF> => ({
|
||||
...createMockCollectionFile(extra, ...fields),
|
||||
i18n: extra.i18n,
|
||||
});
|
||||
|
||||
export const createMockFilesCollection = <EF extends BaseField>(
|
||||
extra: Omit<Partial<FilesCollection<EF>>, 'files'> & Pick<FilesCollection<EF>, 'files'>,
|
||||
): FilesCollection<EF> => ({
|
||||
name: 'mock_collection',
|
||||
label: 'Mock Collections',
|
||||
label_singular: 'Mock Collection',
|
||||
description:
|
||||
'The description is a great place for tone setting, high level information, and editing guidelines that are specific to a collection.\n',
|
||||
summary: '{{title}}',
|
||||
sortable_fields: {
|
||||
fields: ['title'],
|
||||
default: {
|
||||
field: 'title',
|
||||
},
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export const createMockFilesCollectionWithDefaults = <EF extends BaseField>(
|
||||
extra: Omit<Partial<FilesCollectionWithDefaults<EF>>, 'files'> &
|
||||
Pick<FilesCollectionWithDefaults<EF>, 'files'>,
|
||||
): FilesCollectionWithDefaults<EF> => ({
|
||||
...createMockFilesCollection(extra),
|
||||
i18n: extra.i18n,
|
||||
files: extra.files,
|
||||
});
|
||||
|
@ -1,11 +1,20 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { applyDefaults } from '@staticcms/core/actions/config';
|
||||
|
||||
import type { BaseField, Config } from '@staticcms/core';
|
||||
import type { BaseField, Config, ConfigWithDefaults } from '@staticcms/core';
|
||||
|
||||
export const createNoDefaultsMockConfig = <EF extends BaseField>(
|
||||
options: Omit<Partial<Config<EF>>, 'collections'> & Pick<Config<EF>, 'collections'>,
|
||||
): Config<EF> => ({
|
||||
backend: {
|
||||
name: 'test-repo',
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
export const createMockConfig = <EF extends BaseField>(
|
||||
options: Omit<Partial<Config<EF>>, 'collections'> & Pick<Config<EF>, 'collections'>,
|
||||
): Config<EF> =>
|
||||
): ConfigWithDefaults<EF> =>
|
||||
applyDefaults({
|
||||
backend: {
|
||||
name: 'test-repo',
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import type { Entry } from '@staticcms/core';
|
||||
import { WorkflowStatus } from '@staticcms/core/constants/publishModes';
|
||||
|
||||
import type { Entry, UnpublishedEntry } from '@staticcms/core';
|
||||
|
||||
export const createMockEntry = (
|
||||
options: Omit<Partial<Entry>, 'data'> & Pick<Entry, 'data'>,
|
||||
@ -14,5 +15,38 @@ export const createMockEntry = (
|
||||
mediaFiles: [],
|
||||
author: 'Some Person',
|
||||
updatedOn: '20230-02-09T00:00:00.000Z',
|
||||
openAuthoring: false,
|
||||
...options,
|
||||
});
|
||||
|
||||
export const createMockExpandedEntry = (
|
||||
options: Omit<Partial<Entry>, 'data'> & Pick<Entry, 'data'> & { field: string },
|
||||
): Entry & { field: string } => ({
|
||||
collection: 'mock_collection',
|
||||
slug: 'slug-value',
|
||||
path: '/path/to/entry',
|
||||
partial: false,
|
||||
raw: JSON.stringify(options.data),
|
||||
label: 'Entry',
|
||||
isModification: false,
|
||||
mediaFiles: [],
|
||||
author: 'Some Person',
|
||||
updatedOn: '20230-02-09T00:00:00.000Z',
|
||||
openAuthoring: false,
|
||||
...options,
|
||||
});
|
||||
|
||||
export const createMockUnpublishedEntry = (
|
||||
options: Partial<UnpublishedEntry>,
|
||||
): UnpublishedEntry => ({
|
||||
slug: 'unpublished-entry.md',
|
||||
collection: 'posts',
|
||||
status: WorkflowStatus.DRAFT,
|
||||
diffs: [
|
||||
{ id: 'index.md', path: 'src/posts/index.md', newFile: false },
|
||||
{ id: 'netlify.png', path: 'netlify.png', newFile: true },
|
||||
],
|
||||
updatedAt: '20230-02-09T00:00:00.000Z',
|
||||
openAuthoring: false,
|
||||
...options,
|
||||
});
|
||||
|
@ -8,7 +8,8 @@ import type {
|
||||
NumberField,
|
||||
RelationField,
|
||||
SelectField,
|
||||
StringOrTextField,
|
||||
StringField,
|
||||
TextField,
|
||||
UUIDField,
|
||||
} from '@staticcms/core';
|
||||
|
||||
@ -66,7 +67,7 @@ export const mockMarkdownField: MarkdownField = {
|
||||
label: 'Body',
|
||||
name: 'body',
|
||||
widget: 'markdown',
|
||||
hint: 'Main content goes here.',
|
||||
hint: '*Main* __content__ __*goes*__ [here](https://example.com/).',
|
||||
};
|
||||
|
||||
export const mockNumberField: NumberField = {
|
||||
@ -92,13 +93,13 @@ export const mockSelectField: SelectField = {
|
||||
options: ['Option 1', 'Option 2', 'Option 3'],
|
||||
};
|
||||
|
||||
export const mockStringField: StringOrTextField = {
|
||||
export const mockStringField: StringField = {
|
||||
label: 'String',
|
||||
name: 'mock_string',
|
||||
widget: 'string',
|
||||
};
|
||||
|
||||
export const mockTextField: StringOrTextField = {
|
||||
export const mockTextField: TextField = {
|
||||
label: 'Text',
|
||||
name: 'mock_text',
|
||||
widget: 'text',
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { createMockCollection } from './collections.mock';
|
||||
import { applyDefaults } from '@staticcms/core/actions/config';
|
||||
import { createMockFolderCollection } from './collections.mock';
|
||||
import { createMockConfig } from './config.mock';
|
||||
import { createMockEntry } from './entry.mock';
|
||||
|
||||
@ -34,7 +35,7 @@ export const createMockWidgetControlProps = <T, F extends BaseField = UnknownFie
|
||||
|
||||
const value = rawValue ?? null;
|
||||
|
||||
const collection = rawCollection ?? createMockCollection({}, options.field);
|
||||
const collection = rawCollection ?? createMockFolderCollection({}, options.field);
|
||||
const config = rawConfig ?? createMockConfig({ collections: [collection] });
|
||||
const entry = rawEntry ?? createMockEntry({ data: { [options.field.name]: value } });
|
||||
|
||||
@ -44,10 +45,12 @@ export const createMockWidgetControlProps = <T, F extends BaseField = UnknownFie
|
||||
rawFieldsErrors ?? (rawErrors ? { [`${path}.${options.field.name}`]: errors } : {});
|
||||
const hasErrors = Boolean(rawErrors && rawErrors.length > 0);
|
||||
|
||||
const configWithDefaults = applyDefaults(config);
|
||||
|
||||
return {
|
||||
label: 'Mock Widget',
|
||||
config,
|
||||
collection,
|
||||
config: configWithDefaults,
|
||||
collection: configWithDefaults.collections[0],
|
||||
collectionFile: undefined,
|
||||
entry,
|
||||
value,
|
||||
@ -64,7 +67,6 @@ export const createMockWidgetControlProps = <T, F extends BaseField = UnknownFie
|
||||
i18n: undefined,
|
||||
duplicate: false,
|
||||
controlled: false,
|
||||
theme: 'light',
|
||||
onChange: jest.fn(),
|
||||
clearChildValidation: jest.fn(),
|
||||
query: jest.fn(),
|
||||
|
@ -7,7 +7,7 @@ import { store } from '@staticcms/core/store';
|
||||
import { createMockWidgetControlProps } from '@staticcms/test/data/widgets.mock';
|
||||
import { renderWithProviders } from '@staticcms/test/test-utils';
|
||||
|
||||
import type { BaseField, UnknownField, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { BaseField, ObjectValue, UnknownField, WidgetControlProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
export interface WidgetControlHarnessOptions {
|
||||
@ -15,16 +15,39 @@ export interface WidgetControlHarnessOptions {
|
||||
withMediaLibrary?: boolean;
|
||||
}
|
||||
|
||||
export type WidgetControlHarnessParams<T, F extends BaseField = UnknownField> = Parameters<
|
||||
typeof createMockWidgetControlProps<T, F>
|
||||
>[0];
|
||||
export type WidgetControlHarnessProps<T, F extends BaseField = UnknownField> = Omit<
|
||||
WidgetControlHarnessParams<T, F>,
|
||||
'field'
|
||||
> &
|
||||
Pick<Partial<WidgetControlHarnessParams<T, F>>, 'field'>;
|
||||
|
||||
export interface WidgetControlHarnessReturn<T, F extends BaseField = UnknownField>
|
||||
extends Omit<ReturnType<typeof renderWithProviders>, 'rerender'> {
|
||||
rerender: (rerenderProps?: Omit<WidgetControlHarnessProps<T, F>, 'field'> | undefined) => {
|
||||
props: Omit<WidgetControlHarnessProps<T, F>, 'field'> | undefined;
|
||||
};
|
||||
store: typeof store;
|
||||
props: WidgetControlProps<T, F, ObjectValue>;
|
||||
}
|
||||
|
||||
export type WidgetControlHarness<T, F extends BaseField = UnknownField> = (
|
||||
renderProps?: WidgetControlHarnessProps<T, F>,
|
||||
renderOptions?: WidgetControlHarnessOptions,
|
||||
) => WidgetControlHarnessReturn<T, F>;
|
||||
|
||||
export const createWidgetControlHarness = <T, F extends BaseField = UnknownField>(
|
||||
Component: FC<WidgetControlProps<T, F>>,
|
||||
defaults: Omit<Partial<WidgetControlProps<T, F>>, 'field'> &
|
||||
Pick<WidgetControlProps<T, F>, 'field'>,
|
||||
options?: WidgetControlHarnessOptions,
|
||||
) => {
|
||||
type Params = Parameters<typeof createMockWidgetControlProps<T, F>>[0];
|
||||
type Props = Omit<Params, 'field'> & Pick<Partial<Params>, 'field'>;
|
||||
|
||||
return (renderProps?: Props, renderOptions?: WidgetControlHarnessOptions) => {
|
||||
): WidgetControlHarness<T, F> => {
|
||||
return (
|
||||
renderProps?: WidgetControlHarnessProps<T, F>,
|
||||
renderOptions?: WidgetControlHarnessOptions,
|
||||
) => {
|
||||
const { useFakeTimers = false, withMediaLibrary = false } = renderOptions ?? options ?? {};
|
||||
if (useFakeTimers) {
|
||||
jest.useFakeTimers({ now: new Date(2023, 1, 12, 10, 15, 35, 0) });
|
||||
@ -49,7 +72,7 @@ export const createWidgetControlHarness = <T, F extends BaseField = UnknownField
|
||||
});
|
||||
}
|
||||
|
||||
const rerender = (rerenderProps?: Omit<Props, 'field'>) => {
|
||||
const rerender = (rerenderProps?: Omit<WidgetControlHarnessProps<T, F>, 'field'>) => {
|
||||
const finalRerenderProps = {
|
||||
...props,
|
||||
...rerenderProps,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||
|
||||
export const createMockRequest = <T>(
|
||||
status: number,
|
||||
data: {
|
||||
@ -35,44 +37,87 @@ export const createMockRequest = <T>(
|
||||
} as Response;
|
||||
};
|
||||
|
||||
export type FetchMethod = 'GET' | 'POST' | 'PUT' | 'HEAD';
|
||||
|
||||
export type QueryCheckFunc = (query: URLSearchParams) => boolean;
|
||||
|
||||
export interface RequestData {
|
||||
query: string | true | QueryCheckFunc;
|
||||
response: MockResponse<unknown> | MockResponseFunc<unknown>;
|
||||
limit?: number;
|
||||
used?: number;
|
||||
}
|
||||
|
||||
export type ReplyFunc = <T>(response: MockResponse<T> | MockResponseFunc<T>) => void;
|
||||
export type RepeatFunc = (limit: number) => { reply: ReplyFunc };
|
||||
|
||||
export interface MockFetch {
|
||||
baseUrl: string;
|
||||
mocks: Record<string, Response>;
|
||||
when: (url: string) => {
|
||||
reply: <T>(
|
||||
status: number,
|
||||
data: {
|
||||
json?: T;
|
||||
text?: string;
|
||||
},
|
||||
options?: {
|
||||
contentType?: string;
|
||||
headers?: Record<string, string>;
|
||||
},
|
||||
) => void;
|
||||
mocks: Record<string, Partial<Record<FetchMethod, RequestData[]>>>;
|
||||
when: (
|
||||
method: FetchMethod,
|
||||
url: string,
|
||||
) => {
|
||||
query: (query: string | true | QueryCheckFunc) => { reply: ReplyFunc; repeat: RepeatFunc };
|
||||
repeat: RepeatFunc;
|
||||
reply: ReplyFunc;
|
||||
};
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export interface MockResponse<T> {
|
||||
status: number;
|
||||
json?: T;
|
||||
text?: string;
|
||||
contentType?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type MockResponseFunc<T> = (url: string) => MockResponse<T> | Promise<MockResponse<T>>;
|
||||
|
||||
const mockFetch = (baseUrl: string): MockFetch => {
|
||||
const mockedFetch: MockFetch = {
|
||||
baseUrl,
|
||||
mocks: {},
|
||||
when(this: MockFetch, url: string) {
|
||||
// eslint-disable-next-line object-shorthand
|
||||
when: function (this: MockFetch, method: FetchMethod, url: string) {
|
||||
const reply =
|
||||
(query: string | true | QueryCheckFunc = '') =>
|
||||
(limit?: number) =>
|
||||
<T>(response: MockResponse<T> | MockResponseFunc<T>) => {
|
||||
const fullUrl = `${baseUrl}${url}`;
|
||||
if (!(fullUrl in this.mocks)) {
|
||||
this.mocks[fullUrl] = {};
|
||||
}
|
||||
|
||||
if (!(method in this.mocks[fullUrl])) {
|
||||
this.mocks[fullUrl][method] = [];
|
||||
}
|
||||
|
||||
this.mocks[fullUrl][method]?.push({
|
||||
query,
|
||||
response,
|
||||
limit,
|
||||
});
|
||||
};
|
||||
|
||||
const repeat =
|
||||
(query: string | true | QueryCheckFunc = '') =>
|
||||
(limit: number) => {
|
||||
return {
|
||||
reply: reply(query)(limit),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
reply: <T>(
|
||||
status: number,
|
||||
data: {
|
||||
json?: T;
|
||||
text?: string;
|
||||
},
|
||||
options?: {
|
||||
contentType?: string;
|
||||
headers?: Record<string, string>;
|
||||
},
|
||||
) => {
|
||||
this.mocks[`${baseUrl}${url}`] = createMockRequest(status, data, options);
|
||||
query: (query: string | true | QueryCheckFunc) => {
|
||||
return {
|
||||
repeat: repeat(query),
|
||||
reply: reply(query)(),
|
||||
};
|
||||
},
|
||||
repeat: repeat(),
|
||||
reply: reply()(),
|
||||
};
|
||||
},
|
||||
reset(this: MockFetch) {
|
||||
@ -80,9 +125,66 @@ const mockFetch = (baseUrl: string): MockFetch => {
|
||||
},
|
||||
};
|
||||
|
||||
global.fetch = jest.fn().mockImplementation((url: string) => {
|
||||
return Promise.resolve(mockedFetch.mocks[url.split('?')[0]]);
|
||||
});
|
||||
global.fetch = jest
|
||||
.fn()
|
||||
.mockImplementation(async (fullUrl: string, { method = 'GET' }: { method: FetchMethod }) => {
|
||||
const [url, ...rest] = fullUrl.split('?');
|
||||
const query = rest.length > 0 ? rest[0] : '';
|
||||
|
||||
const mockResponses = [...(mockedFetch.mocks[url]?.[method] ?? [])];
|
||||
if (!mockResponses) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
for (let i = 0; i < mockResponses.length; i++) {
|
||||
const mockResponse = mockResponses[i];
|
||||
|
||||
const limit = mockResponse.limit;
|
||||
const used = mockResponse.used ?? 0;
|
||||
|
||||
if (isNotNullish(limit)) {
|
||||
if (used >= limit) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNotNullish(mockResponse.query) && mockResponse.query !== true) {
|
||||
if (typeof mockResponse.query === 'string') {
|
||||
if (mockResponse.query !== query) {
|
||||
continue;
|
||||
}
|
||||
} else if (!mockResponse.query(new URLSearchParams(query))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let responseData = mockResponse.response;
|
||||
if (typeof responseData === 'function') {
|
||||
responseData = await responseData(fullUrl);
|
||||
}
|
||||
|
||||
const response = createMockRequest(
|
||||
responseData.status,
|
||||
{
|
||||
json: responseData.json,
|
||||
text: responseData.text,
|
||||
},
|
||||
{
|
||||
contentType: responseData.contentType,
|
||||
headers: responseData.headers,
|
||||
},
|
||||
);
|
||||
|
||||
mockedFetch.mocks[url][method]![i] = {
|
||||
...mockResponse,
|
||||
used: used + 1,
|
||||
};
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
return mockedFetch;
|
||||
};
|
||||
|
@ -10,7 +10,24 @@ if (typeof window === 'undefined') {
|
||||
navigator: {
|
||||
platform: 'Win',
|
||||
},
|
||||
matchMedia: () => ({
|
||||
matches: false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
global.URL.createObjectURL = jest.fn();
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // Deprecated
|
||||
removeListener: jest.fn(), // Deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
Reference in New Issue
Block a user