@jpmorganchase/elemental 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. package/.storybook/main.js +1 -0
  2. package/.storybook/manager.js +1 -0
  3. package/.storybook/preview.jsx +3 -0
  4. package/LICENSE +190 -0
  5. package/README.md +19 -0
  6. package/jest.config.js +7 -0
  7. package/package.json +111 -0
  8. package/src/__fixtures__/api-descriptions/Instagram.ts +1859 -0
  9. package/src/__fixtures__/api-descriptions/badgesForSchema.ts +36 -0
  10. package/src/__fixtures__/api-descriptions/simpleApiWithInternalOperations.ts +253 -0
  11. package/src/__fixtures__/api-descriptions/simpleApiWithoutDescription.ts +243 -0
  12. package/src/__fixtures__/api-descriptions/todosApiBundled.ts +430 -0
  13. package/src/__fixtures__/api-descriptions/zoomApiYaml.ts +6083 -0
  14. package/src/components/API/APIWithSidebarLayout.tsx +111 -0
  15. package/src/components/API/APIWithStackedLayout.tsx +220 -0
  16. package/src/components/API/__tests__/utils.test.ts +848 -0
  17. package/src/components/API/utils.ts +174 -0
  18. package/src/containers/API.spec.tsx +131 -0
  19. package/src/containers/API.stories.tsx +99 -0
  20. package/src/containers/API.tsx +200 -0
  21. package/src/hooks/useExportDocumentProps.spec.tsx +68 -0
  22. package/src/hooks/useExportDocumentProps.tsx +48 -0
  23. package/src/index.ts +2 -0
  24. package/src/styles.css +1 -0
  25. package/src/utils/oas/__tests__/oas.spec.ts +272 -0
  26. package/src/utils/oas/index.ts +150 -0
  27. package/src/utils/oas/oas2.ts +31 -0
  28. package/src/utils/oas/oas3.ts +37 -0
  29. package/src/utils/oas/types.ts +31 -0
  30. package/src/web-components/__stories__/Api.stories.tsx +63 -0
  31. package/src/web-components/components.ts +20 -0
  32. package/src/web-components/index.ts +3 -0
  33. package/tsconfig.build.json +18 -0
  34. package/tsconfig.json +7 -0
  35. package/web-components.config.js +1 -0
@@ -0,0 +1,174 @@
1
+ import { isHttpOperation, isHttpService, TableOfContentsItem } from '@stoplight/elements-core';
2
+ import { NodeType } from '@stoplight/types';
3
+ import { defaults } from 'lodash';
4
+
5
+ import { OperationNode, ServiceChildNode, ServiceNode } from '../../utils/oas/types';
6
+
7
+ export type TagGroup = { title: string; items: OperationNode[] };
8
+
9
+ export const computeTagGroups = (serviceNode: ServiceNode) => {
10
+ const groupsByTagId: { [tagId: string]: TagGroup } = {};
11
+ const ungrouped = [];
12
+
13
+ const lowerCaseServiceTags = serviceNode.tags.map(tn => tn.toLowerCase());
14
+
15
+ for (const node of serviceNode.children) {
16
+ if (node.type !== NodeType.HttpOperation) continue;
17
+ const tagName = node.tags[0];
18
+
19
+ if (tagName) {
20
+ const tagId = tagName.toLowerCase();
21
+ if (groupsByTagId[tagId]) {
22
+ groupsByTagId[tagId].items.push(node);
23
+ } else {
24
+ const serviceTagIndex = lowerCaseServiceTags.findIndex(tn => tn === tagId);
25
+ const serviceTagName = serviceNode.tags[serviceTagIndex];
26
+ groupsByTagId[tagId] = {
27
+ title: serviceTagName || tagName,
28
+ items: [node],
29
+ };
30
+ }
31
+ } else {
32
+ ungrouped.push(node);
33
+ }
34
+ }
35
+
36
+ const orderedTagGroups = Object.entries(groupsByTagId)
37
+ .sort(([g1], [g2]) => {
38
+ const g1LC = g1.toLowerCase();
39
+ const g2LC = g2.toLowerCase();
40
+ const g1Idx = lowerCaseServiceTags.findIndex(tn => tn === g1LC);
41
+ const g2Idx = lowerCaseServiceTags.findIndex(tn => tn === g2LC);
42
+
43
+ // Move not-tagged groups to the bottom
44
+ if (g1Idx < 0 && g2Idx < 0) return 0;
45
+ if (g1Idx < 0) return 1;
46
+ if (g2Idx < 0) return -1;
47
+
48
+ // sort tagged groups according to the order found in HttpService
49
+ return g1Idx - g2Idx;
50
+ })
51
+ .map(([, tagGroup]) => tagGroup);
52
+
53
+ return { groups: orderedTagGroups, ungrouped };
54
+ };
55
+
56
+ interface ComputeAPITreeConfig {
57
+ hideSchemas?: boolean;
58
+ hideInternal?: boolean;
59
+ }
60
+
61
+ const defaultComputerAPITreeConfig = {
62
+ hideSchemas: false,
63
+ hideInternal: false,
64
+ };
65
+
66
+ export const computeAPITree = (serviceNode: ServiceNode, config: ComputeAPITreeConfig = {}) => {
67
+ const mergedConfig = defaults(config, defaultComputerAPITreeConfig);
68
+ const tree: TableOfContentsItem[] = [];
69
+
70
+ tree.push({
71
+ id: '/',
72
+ slug: '/',
73
+ title: 'Overview',
74
+ type: 'overview',
75
+ meta: '',
76
+ });
77
+
78
+ const operationNodes = serviceNode.children.filter(node => node.type === NodeType.HttpOperation);
79
+ if (operationNodes.length) {
80
+ tree.push({
81
+ title: 'Endpoints',
82
+ });
83
+
84
+ const { groups, ungrouped } = computeTagGroups(serviceNode);
85
+
86
+ // Show ungroupped operations above tag groups
87
+ ungrouped.forEach(operationNode => {
88
+ if (mergedConfig.hideInternal && operationNode.data.internal) {
89
+ return;
90
+ }
91
+ tree.push({
92
+ id: operationNode.uri,
93
+ slug: operationNode.uri,
94
+ title: operationNode.name,
95
+ type: operationNode.type,
96
+ meta: operationNode.data.method,
97
+ });
98
+ });
99
+
100
+ groups.forEach(group => {
101
+ const items = group.items.flatMap(operationNode => {
102
+ if (mergedConfig.hideInternal && operationNode.data.internal) {
103
+ return [];
104
+ }
105
+ return {
106
+ id: operationNode.uri,
107
+ slug: operationNode.uri,
108
+ title: operationNode.name,
109
+ type: operationNode.type,
110
+ meta: operationNode.data.method,
111
+ };
112
+ });
113
+ if (items.length > 0) {
114
+ tree.push({
115
+ title: group.title,
116
+ items,
117
+ });
118
+ }
119
+ });
120
+ }
121
+
122
+ let schemaNodes = serviceNode.children.filter(node => node.type === NodeType.Model);
123
+ if (mergedConfig.hideInternal) {
124
+ schemaNodes = schemaNodes.filter(node => !node.data['x-internal']);
125
+ }
126
+
127
+ if (!mergedConfig.hideSchemas && schemaNodes.length) {
128
+ tree.push({
129
+ title: 'Schemas',
130
+ });
131
+
132
+ schemaNodes.forEach(node => {
133
+ tree.push({
134
+ id: node.uri,
135
+ slug: node.uri,
136
+ title: node.name,
137
+ type: node.type,
138
+ meta: '',
139
+ });
140
+ });
141
+ }
142
+ return tree;
143
+ };
144
+
145
+ export const findFirstNodeSlug = (tree: TableOfContentsItem[]): string | void => {
146
+ for (const item of tree) {
147
+ if ('slug' in item) {
148
+ return item.slug;
149
+ }
150
+
151
+ if ('items' in item) {
152
+ const slug = findFirstNodeSlug(item.items);
153
+ if (slug) {
154
+ return slug;
155
+ }
156
+ }
157
+ }
158
+
159
+ return;
160
+ };
161
+
162
+ export const isInternal = (node: ServiceChildNode | ServiceNode): boolean => {
163
+ const data = node.data;
164
+
165
+ if (isHttpOperation(data)) {
166
+ return !!data.internal;
167
+ }
168
+
169
+ if (isHttpService(data)) {
170
+ return false;
171
+ }
172
+
173
+ return !!data['x-internal'];
174
+ };
@@ -0,0 +1,131 @@
1
+ import '@testing-library/jest-dom';
2
+
3
+ import {
4
+ withMosaicProvider,
5
+ withPersistenceBoundary,
6
+ withQueryClientProvider,
7
+ withStyles,
8
+ } from '@stoplight/elements-core';
9
+ import { render, screen } from '@testing-library/react';
10
+ import { createMemoryHistory } from 'history';
11
+ import { flow } from 'lodash';
12
+ import * as React from 'react';
13
+ import { Route, Router } from 'react-router';
14
+
15
+ import { InstagramAPI } from '../__fixtures__/api-descriptions/Instagram';
16
+ import { simpleApiWithoutDescription } from '../__fixtures__/api-descriptions/simpleApiWithoutDescription';
17
+ import { API, APIImpl } from './API';
18
+
19
+ export const APIWithoutRouter = flow(
20
+ withStyles,
21
+ withPersistenceBoundary,
22
+ withMosaicProvider,
23
+ withQueryClientProvider,
24
+ )(APIImpl);
25
+
26
+ describe('API', () => {
27
+ const APIDocument = {
28
+ ...InstagramAPI,
29
+ info: {
30
+ ...InstagramAPI.info,
31
+ 'x-logo': {
32
+ ...InstagramAPI.info['x-logo'],
33
+ altText: 'instagram-logo',
34
+ },
35
+ },
36
+ };
37
+
38
+ // we need to add scrollTo to the Element prototype before we mount so it has the method available
39
+ Element.prototype.scrollTo = () => {};
40
+
41
+ it('displays logo specified in x-logo property of API document', async () => {
42
+ render(<API layout="sidebar" apiDescriptionDocument={InstagramAPI} />);
43
+
44
+ // checks if altText defaults to "logo" if the prop is not passed in API document
45
+ // checks if logo is present
46
+ expect(await screen.findByAltText('logo')).toBeInTheDocument();
47
+ });
48
+
49
+ it('uses the altText property from the API document', async () => {
50
+ render(<API layout="sidebar" apiDescriptionDocument={APIDocument} />);
51
+
52
+ expect(await screen.findByAltText('instagram-logo')).toBeInTheDocument();
53
+ });
54
+
55
+ it("doesn't display the logo when no properties are passed neither via API document nor as component prop", () => {
56
+ render(<API layout="sidebar" apiDescriptionDocument={simpleApiWithoutDescription} />);
57
+
58
+ expect(screen.queryByAltText('logo')).not.toBeInTheDocument();
59
+ });
60
+
61
+ it('overrides the logo from API document with the one passed in a prop', async () => {
62
+ render(<API logo="thisisarequiredprop" layout="sidebar" apiDescriptionDocument={APIDocument} />);
63
+
64
+ expect(screen.queryByAltText('instagram-logo')).not.toBeInTheDocument();
65
+ expect(await screen.findByAltText('logo')).toBeInTheDocument();
66
+ });
67
+
68
+ it('displays internal operations by default', () => {
69
+ const history = createMemoryHistory();
70
+ history.push('/paths/internal-operation/get');
71
+
72
+ const { unmount } = render(
73
+ <Router history={history}>
74
+ <Route path="/">
75
+ <APIWithoutRouter layout="sidebar" apiDescriptionDocument={APIDocument} />
76
+ </Route>
77
+ </Router>,
78
+ );
79
+
80
+ expect(screen.getByText('If you see this, something went wrong')).toBeInTheDocument();
81
+
82
+ unmount();
83
+ });
84
+
85
+ it('displays internal models by default', () => {
86
+ const history = createMemoryHistory();
87
+ history.push('/schemas/InternalObject');
88
+
89
+ render(
90
+ <Router history={history}>
91
+ <Route path="/">
92
+ <APIWithoutRouter layout="sidebar" apiDescriptionDocument={APIDocument} />
93
+ </Route>
94
+ </Router>,
95
+ );
96
+
97
+ expect(screen.getByText('Cool object, but internal.')).toBeInTheDocument();
98
+ });
99
+
100
+ it('reroutes to main page on internal operation if hideInternal is on', () => {
101
+ const history = createMemoryHistory();
102
+ history.push('/paths/internal-operation/get');
103
+
104
+ render(
105
+ <Router history={history}>
106
+ <Route path="/">
107
+ <APIWithoutRouter layout="sidebar" apiDescriptionDocument={APIDocument} hideInternal />
108
+ </Route>
109
+ </Router>,
110
+ );
111
+
112
+ expect(screen.queryByText('If you see this, something went wrong')).not.toBeInTheDocument();
113
+ expect(history.location.pathname).toBe('/');
114
+ });
115
+
116
+ it('reroutes to main page on internal model if hideInternal is on', () => {
117
+ const history = createMemoryHistory();
118
+ history.push('/schemas/InternalObject');
119
+
120
+ render(
121
+ <Router history={history}>
122
+ <Route path="/">
123
+ <APIWithoutRouter layout="sidebar" apiDescriptionDocument={APIDocument} hideInternal />
124
+ </Route>
125
+ </Router>,
126
+ );
127
+
128
+ expect(screen.queryByText('Cool object, but internal.')).not.toBeInTheDocument();
129
+ expect(history.location.pathname).toBe('/');
130
+ });
131
+ });
@@ -0,0 +1,99 @@
1
+ import { parse } from '@stoplight/yaml';
2
+ import { Story } from '@storybook/react';
3
+ import * as React from 'react';
4
+
5
+ import { badgesForSchema } from '../__fixtures__/api-descriptions/badgesForSchema';
6
+ import { simpleApiWithInternalOperations } from '../__fixtures__/api-descriptions/simpleApiWithInternalOperations';
7
+ import { simpleApiWithoutDescription } from '../__fixtures__/api-descriptions/simpleApiWithoutDescription';
8
+ import { todosApiBundled } from '../__fixtures__/api-descriptions/todosApiBundled';
9
+ import { zoomApiYaml } from '../__fixtures__/api-descriptions/zoomApiYaml';
10
+ import { API, APIProps } from './API';
11
+
12
+ export default {
13
+ title: 'Public/API',
14
+ component: API,
15
+ argTypes: {
16
+ apiDescriptionDocument: { control: 'text', type: { required: false }, table: { category: 'Input' } },
17
+ apiDescriptionUrl: { control: 'text', table: { category: 'Input' } },
18
+ layout: {
19
+ control: { type: 'inline-radio' },
20
+ table: { category: 'UI' },
21
+ },
22
+ basePath: { table: { category: 'Routing' } },
23
+ router: { table: { category: 'Routing' } },
24
+ },
25
+ args: {
26
+ router: 'memory',
27
+ },
28
+ };
29
+
30
+ const Template: Story<APIProps> = args => <API {...args} />;
31
+
32
+ export const APIWithYamlProvidedDirectly = Template.bind({});
33
+ APIWithYamlProvidedDirectly.args = {
34
+ apiDescriptionDocument: zoomApiYaml,
35
+ };
36
+ APIWithYamlProvidedDirectly.storyName = 'Direct YAML Input (Zoom)';
37
+
38
+ export const APIWithJSONProvidedDirectly = Template.bind({});
39
+ APIWithJSONProvidedDirectly.args = {
40
+ apiDescriptionDocument: JSON.stringify(parse(zoomApiYaml), null, ' '),
41
+ };
42
+ APIWithJSONProvidedDirectly.storyName = 'Direct JSON Input (Zoom)';
43
+
44
+ export const APIWithoutDescription = Template.bind({});
45
+ APIWithoutDescription.args = {
46
+ apiDescriptionDocument: JSON.stringify(simpleApiWithoutDescription, null, 2),
47
+ };
48
+ APIWithoutDescription.storyName = 'API Without Description';
49
+
50
+ export const APIWithInternalOperations = Template.bind({});
51
+ APIWithInternalOperations.args = {
52
+ apiDescriptionDocument: JSON.stringify(simpleApiWithInternalOperations, null, 2),
53
+ };
54
+ APIWithInternalOperations.storyName = 'API With Internal Operations';
55
+
56
+ export const OpenApi3Schema = Template.bind({});
57
+ OpenApi3Schema.args = {
58
+ apiDescriptionDocument: todosApiBundled,
59
+ };
60
+ OpenApi3Schema.storyName = 'Open Api 3.0 Schema';
61
+
62
+ export const BadgesForSchema = Template.bind({});
63
+ BadgesForSchema.args = {
64
+ apiDescriptionDocument: badgesForSchema,
65
+ };
66
+ BadgesForSchema.storyName = 'Badges For Schema';
67
+
68
+ export const StackedLayout = Template.bind({});
69
+ StackedLayout.args = {
70
+ apiDescriptionDocument: JSON.stringify(parse(zoomApiYaml), null, ' '),
71
+ layout: 'stacked',
72
+ };
73
+ StackedLayout.storyName = 'Stacked Layout (Zoom)';
74
+
75
+ export const Box = Template.bind({});
76
+ Box.args = {
77
+ apiDescriptionUrl: 'https://raw.githubusercontent.com/box/box-openapi/main/content/openapi.yml',
78
+ };
79
+ Box.storyName = 'Box';
80
+
81
+ export const DigitalOcean = Template.bind({});
82
+ DigitalOcean.args = {
83
+ apiDescriptionUrl:
84
+ 'https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml',
85
+ };
86
+ DigitalOcean.storyName = 'DigitalOcean';
87
+
88
+ export const Github = Template.bind({});
89
+ Github.args = {
90
+ apiDescriptionUrl:
91
+ 'https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/ghes-3.0/ghes-3.0.json',
92
+ };
93
+ Github.storyName = 'GitHub';
94
+
95
+ export const Instagram = Template.bind({});
96
+ Instagram.args = {
97
+ apiDescriptionUrl: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml',
98
+ };
99
+ Instagram.storyName = 'Instagram';
@@ -0,0 +1,200 @@
1
+ import {
2
+ InlineRefResolverProvider,
3
+ NonIdealState,
4
+ RoutingProps,
5
+ useBundleRefsIntoDocument,
6
+ useParsedValue,
7
+ withMosaicProvider,
8
+ withPersistenceBoundary,
9
+ withQueryClientProvider,
10
+ withRouter,
11
+ withStyles,
12
+ } from '@stoplight/elements-core';
13
+ import { Box, Flex, Icon } from '@stoplight/mosaic';
14
+ import { flow } from 'lodash';
15
+ import * as React from 'react';
16
+ import { useQuery } from 'react-query';
17
+
18
+ import { APIWithSidebarLayout } from '../components/API/APIWithSidebarLayout';
19
+ import { APIWithStackedLayout } from '../components/API/APIWithStackedLayout';
20
+ import { useExportDocumentProps } from '../hooks/useExportDocumentProps';
21
+ import { transformOasToServiceNode } from '../utils/oas';
22
+
23
+ export type APIProps = APIPropsWithDocument | APIPropsWithUrl;
24
+
25
+ export type APIPropsWithUrl = {
26
+ /**
27
+ * Specify the URL of the input OAS2/3 document here.
28
+ *
29
+ * Mutually exclusive with `apiDescriptionDocument`.
30
+ */
31
+ apiDescriptionUrl: string;
32
+ } & CommonAPIProps;
33
+ export type APIPropsWithDocument = {
34
+ /**
35
+ * You can specify the input OAS2/3 document here directly in JSON or YAML format.
36
+ *
37
+ * Mutually exclusive with `apiDescriptionUrl`.
38
+ */
39
+ apiDescriptionDocument: string | object;
40
+ apiDescriptionUrl?: string;
41
+ } & CommonAPIProps;
42
+
43
+ export interface CommonAPIProps extends RoutingProps {
44
+ /**
45
+ * The API component supports two layout options.
46
+ *
47
+ * - Sidebar: Navigation on the left side, resembles Stoplight Platform.
48
+ * - Stacked: No sidebar, resembles the structure of Swagger UI.
49
+ *
50
+ * @default "sidebar"
51
+ */
52
+ layout?: 'sidebar' | 'stacked';
53
+ logo?: string;
54
+
55
+ /**
56
+ * Allows hiding the TryIt component
57
+ */
58
+ hideTryIt?: boolean;
59
+
60
+ /**
61
+ * Hides schemas from being displayed in Table of Contents
62
+ */
63
+ hideSchemas?: boolean;
64
+
65
+ /**
66
+ * Hides models and operations marked as internal
67
+ * @default false
68
+ */
69
+ hideInternal?: boolean;
70
+
71
+ /**
72
+ * Hides export button from being displayed in overview page
73
+ * @default false
74
+ */
75
+ hideExport?: boolean;
76
+
77
+ /**
78
+ * Fetch credentials policy for TryIt component
79
+ * For more information: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
80
+ * @default "omit"
81
+ */
82
+
83
+ tryItCredentialsPolicy?: 'omit' | 'include' | 'same-origin';
84
+
85
+ /**
86
+ * Url of a CORS proxy that will be used to send requests in TryIt.
87
+ * Provided url will be prepended to an URL of an actual request.
88
+ * @default false
89
+ */
90
+ tryItCorsProxy?: string;
91
+ tryItOutDefaultServer?: string;
92
+ }
93
+
94
+ const propsAreWithDocument = (props: APIProps): props is APIPropsWithDocument => {
95
+ return props.hasOwnProperty('apiDescriptionDocument');
96
+ };
97
+
98
+ export const APIImpl: React.FC<APIProps> = props => {
99
+ const {
100
+ layout,
101
+ apiDescriptionUrl = '',
102
+ logo,
103
+ hideTryIt,
104
+ hideSchemas,
105
+ hideInternal,
106
+ hideExport,
107
+ tryItCredentialsPolicy,
108
+ tryItCorsProxy,
109
+ tryItOutDefaultServer,
110
+ } = props;
111
+ const apiDescriptionDocument = propsAreWithDocument(props) ? props.apiDescriptionDocument : undefined;
112
+
113
+ const { data: fetchedDocument, error } = useQuery(
114
+ [apiDescriptionUrl],
115
+ () =>
116
+ fetch(apiDescriptionUrl).then(res => {
117
+ if (res.ok) {
118
+ return res.text();
119
+ }
120
+ throw new Error(`Unable to load description document, status code: ${res.status}`);
121
+ }),
122
+ {
123
+ enabled: apiDescriptionUrl !== '' && !apiDescriptionDocument,
124
+ },
125
+ );
126
+
127
+ const document = apiDescriptionDocument || fetchedDocument || '';
128
+ const parsedDocument = useParsedValue(document);
129
+ const bundledDocument = useBundleRefsIntoDocument(parsedDocument, { baseUrl: apiDescriptionUrl });
130
+ const serviceNode = React.useMemo(() => transformOasToServiceNode(bundledDocument), [bundledDocument]);
131
+ const exportProps = useExportDocumentProps({ originalDocument: document, bundledDocument });
132
+
133
+ if (error) {
134
+ return (
135
+ <Flex justify="center" alignItems="center" w="full" minH="screen">
136
+ <NonIdealState
137
+ title="Document could not be loaded"
138
+ description="The API description document could not be fetched. This could indicate connectivity problems, or issues with the server hosting the spec."
139
+ icon="exclamation-triangle"
140
+ />
141
+ </Flex>
142
+ );
143
+ }
144
+
145
+ if (!bundledDocument) {
146
+ return (
147
+ <Flex justify="center" alignItems="center" w="full" minH="screen" color="light">
148
+ <Box as={Icon} icon={['fal', 'circle-notch']} size="3x" spin />
149
+ </Flex>
150
+ );
151
+ }
152
+
153
+ if (!serviceNode) {
154
+ return (
155
+ <Flex justify="center" alignItems="center" w="full" minH="screen">
156
+ <NonIdealState
157
+ title="Failed to parse OpenAPI file"
158
+ description="Please make sure your OpenAPI file is valid and try again"
159
+ />
160
+ </Flex>
161
+ );
162
+ }
163
+
164
+ return (
165
+ <InlineRefResolverProvider document={parsedDocument}>
166
+ {layout === 'stacked' ? (
167
+ <APIWithStackedLayout
168
+ serviceNode={serviceNode}
169
+ hideTryIt={hideTryIt}
170
+ hideExport={hideExport}
171
+ exportProps={exportProps}
172
+ tryItCredentialsPolicy={tryItCredentialsPolicy}
173
+ tryItCorsProxy={tryItCorsProxy}
174
+ tryItOutDefaultServer={tryItOutDefaultServer}
175
+ />
176
+ ) : (
177
+ <APIWithSidebarLayout
178
+ logo={logo}
179
+ serviceNode={serviceNode}
180
+ hideTryIt={hideTryIt}
181
+ hideSchemas={hideSchemas}
182
+ hideInternal={hideInternal}
183
+ hideExport={hideExport}
184
+ exportProps={exportProps}
185
+ tryItCredentialsPolicy={tryItCredentialsPolicy}
186
+ tryItCorsProxy={tryItCorsProxy}
187
+ tryItOutDefaultServer={tryItOutDefaultServer}
188
+ />
189
+ )}
190
+ </InlineRefResolverProvider>
191
+ );
192
+ };
193
+
194
+ export const API = flow(
195
+ withRouter,
196
+ withStyles,
197
+ withPersistenceBoundary,
198
+ withMosaicProvider,
199
+ withQueryClientProvider,
200
+ )(APIImpl);
@@ -0,0 +1,68 @@
1
+ import '@testing-library/jest-dom';
2
+
3
+ import { safeStringify } from '@stoplight/yaml';
4
+ import { act, renderHook } from '@testing-library/react-hooks';
5
+ import { saveAs } from 'file-saver';
6
+
7
+ import { InstagramAPI as bundledJson } from '../__fixtures__/api-descriptions/Instagram';
8
+ import { simpleApiWithoutDescription as json } from '../__fixtures__/api-descriptions/simpleApiWithoutDescription';
9
+ import { todosApiBundled as bundledYaml } from '../__fixtures__/api-descriptions/todosApiBundled';
10
+ import { zoomApiYaml as yaml } from '../__fixtures__/api-descriptions/zoomApiYaml';
11
+ import { useExportDocumentProps } from './useExportDocumentProps';
12
+
13
+ jest.mock('file-saver');
14
+
15
+ describe('useExportDocumentProps', () => {
16
+ afterEach(() => {
17
+ jest.resetAllMocks();
18
+ });
19
+ it('exports json document', () => {
20
+ const data = renderHook(() =>
21
+ useExportDocumentProps({
22
+ originalDocument: json,
23
+ bundledDocument: bundledJson,
24
+ }),
25
+ );
26
+
27
+ act(() => {
28
+ data.result.current.original.onPress();
29
+ data.result.current.bundled.onPress();
30
+ });
31
+
32
+ const expectedOriginalDocument = new Blob([JSON.stringify(json, null, 2)], {
33
+ type: 'application/json',
34
+ });
35
+
36
+ const expectedBundledDocument = new Blob([JSON.stringify(bundledJson, null, 2)], {
37
+ type: 'application/json',
38
+ });
39
+ expect(saveAs).toBeCalledTimes(2);
40
+ expect(saveAs).toHaveBeenCalledWith(expectedOriginalDocument, 'document.json');
41
+ expect(saveAs).toHaveBeenCalledWith(expectedBundledDocument, 'document.json');
42
+ });
43
+
44
+ it('exports yaml document', () => {
45
+ const data = renderHook(() =>
46
+ useExportDocumentProps({
47
+ originalDocument: safeStringify(yaml),
48
+ bundledDocument: bundledYaml,
49
+ }),
50
+ );
51
+
52
+ act(() => {
53
+ data.result.current.original.onPress();
54
+ data.result.current.bundled.onPress();
55
+ });
56
+
57
+ const expectedOriginalDocument = new Blob([safeStringify(yaml)], {
58
+ type: 'application/yaml',
59
+ });
60
+
61
+ const expectedBundledDocument = new Blob([safeStringify(bundledYaml)], {
62
+ type: 'application/yaml',
63
+ });
64
+ expect(saveAs).toBeCalledTimes(2);
65
+ expect(saveAs).toHaveBeenCalledWith(expectedOriginalDocument, 'document.yaml');
66
+ expect(saveAs).toHaveBeenCalledWith(expectedBundledDocument, 'document.yaml');
67
+ });
68
+ });