@jpmorganchase/elemental 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ });