Skip to content
This repository was archived by the owner on Nov 28, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 94 additions & 51 deletions example/src/Demo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,101 @@ require('../../example/swagger-files/response-schemas.json');

require('../../packages/api-logs/main.css');

function Demo({ fetchSwagger, status, docs, oas, oauth }) {
return (
<div>
<div className="api-list-header">
<ApiList fetchSwagger={fetchSwagger} />
<pre>{status.join('\n')}</pre>
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
brokenExplorerState: false,
maskErrorMessages: false,
};

this.toggleBrokenState = this.toggleBrokenState.bind(this);
this.toggleErrorMasking = this.toggleErrorMasking.bind(this);
}

toggleErrorMasking(e) {
this.setState({ maskErrorMessages: e.target.checked });
this.forceUpdate();
}

toggleBrokenState(e) {
this.setState({ brokenExplorerState: e.target.checked });
this.forceUpdate();
}

render() {
const { fetchSwagger, status, docs, oas, oauth } = this.props;
const { brokenExplorerState, maskErrorMessages } = this.state;

const additionalProps = {};
if (!brokenExplorerState) {
additionalProps.oasFiles = {
'api-setting': Object.assign(extensions.defaults, oas),
};
}

return (
<div>
<div className="api-list-header">
<ApiList fetchSwagger={fetchSwagger} />

{status.length > 0 ? (
<pre>{status.join('\n')}</pre>
) : (
<p>
<input checked={maskErrorMessages} onChange={this.toggleErrorMasking} type="checkbox" /> Mask error
messages? &nbsp;
<input checked={brokenExplorerState} onChange={this.toggleBrokenState} type="checkbox" /> Show fully
broken state
</p>
)}
</div>

{status.length === 0 && (
<ApiExplorer
{...additionalProps}
// Uncomment this in for column layout
// appearance={{ referenceLayout: 'column' }}
appearance={{ referenceLayout: 'row' }}
// Uncomment this if you want to test enterprise-structured URLs
// baseUrl={'/child/v1.0'}
baseUrl="/"
// To test the top level error boundary, uncomment this
// docs={[null, null]}
docs={docs}
// We only really set this to `true` for testing sites for errors using puppeteer
dontLazyLoad={false}
flags={{ correctnewlines: false }}
glossaryTerms={[{ term: 'apiKey', definition: 'This is a definition' }]}
Logs={Logs}
maskErrorMessages={maskErrorMessages}
oauth={oauth}
suggestedEdits
variables={{
// Uncomment this to test without logs
// user: {}
// Uncomment this to test with logs
// user: {
// keys: [
// { id: 'someid', name: 'project1', apiKey: '123' },
// { id: 'anotherid', name: 'project2', apiKey: '456' },
// ],
// },
// Uncomment this to test without keys array
// user: { user: '123456', pass: 'abc', apiKey: '123456' },
user: {
keys: [
{ id: 'asdfghjkl', name: 'project1', apiKey: '123', user: 'user1', pass: 'pass1' },
{ id: 'zxcvbnm', name: 'project2', apiKey: '456', user: 'user2', pass: 'pass2' },
],
},
defaults: [],
}}
/>
)}
</div>
{status.length === 0 && (
<ApiExplorer
// Uncomment this in for column layout
// appearance={{ referenceLayout: 'column' }}
appearance={{ referenceLayout: 'row' }}
// Uncomment this if you want to test enterprise-structured URLs
// baseUrl={'/child/v1.0'}
baseUrl="/"
// To test the top level error boundary, uncomment this
// docs={[null, null]}
docs={docs}
// We only really set this to `true` for testing sites for errors using puppeteer
dontLazyLoad={false}
flags={{ correctnewlines: false }}
glossaryTerms={[{ term: 'apiKey', definition: 'This is a definition' }]}
Logs={Logs}
oasFiles={{
'api-setting': Object.assign(extensions.defaults, oas),
}}
oauth={oauth}
suggestedEdits
variables={{
// Uncomment this to test without logs
// user: {}
// Uncomment this to test with logs
// user: {
// keys: [
// { id: 'someid', name: 'project1', apiKey: '123' },
// { id: 'anotherid', name: 'project2', apiKey: '456' },
// ],
// },
// Uncomment this to test without keys array
// user: { user: '123456', pass: 'abc', apiKey: '123456' },
user: {
keys: [
{ id: 'asdfghjkl', name: 'project1', apiKey: '123', user: 'user1', pass: 'pass1' },
{ id: 'zxcvbnm', name: 'project2', apiKey: '456', user: 'user2', pass: 'pass2' },
],
},
defaults: [],
}}
/>
)}
</div>
);
);
}
}

Demo.propTypes = {
Expand Down
90 changes: 74 additions & 16 deletions packages/api-explorer/__tests__/Doc.test.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const extensions = require('@readme/oas-extensions');
const { Request, Response } = require('node-fetch');
const shortid = require('shortid');

global.Request = Request;

const React = require('react');
const { shallow, mount } = require('enzyme');
const Doc = require('../src/Doc');
const ErrorBoundary = require('../src/ErrorBoundary');

const petstore = require('@readme/oas-examples/3.0/json/petstore.json');
const petstoreWithAuth = require('./__fixtures__/petstore/oas.json');
Expand Down Expand Up @@ -368,7 +370,7 @@ describe('themes', () => {
});
});

test('should output with an error message if the endpoint fails to load', () => {
describe('error handling', () => {
const brokenOas = {
paths: {
'/path': {
Expand All @@ -381,21 +383,77 @@ test('should output with an error message if the endpoint fails to load', () =>
},
};

const doc = mount(
<Doc
{...props}
doc={{
title: 'title',
slug: 'slug',
type: 'endpoint',
swagger: { path: '/path' },
api: { method: 'post' },
}}
oas={brokenOas}
/>
);
const docProps = {
title: 'title',
slug: 'slug',
type: 'endpoint',
swagger: { path: '/path' },
api: { method: 'post' },
};

doc.setState({ showEndpoint: true });
const spy = jest.spyOn(shortid, 'generate');
const originalConsole = console;

// We're testing errors here, so we don't need `console.error` logs spamming the test output.
beforeEach(() => {
// eslint-disable-next-line no-console
console.error = () => {};
});

afterEach(() => {
// eslint-disable-next-line no-console
console.error = originalConsole.error;
});

it('should output with a masked error message if the endpoint fails to load', () => {
const doc = mount(<Doc {...props} doc={docProps} maskErrorMessages={true} oas={brokenOas} />);

doc.setState({ showEndpoint: true });

const html = doc.html();

expect(spy).toHaveBeenCalled();
expect(doc.find(ErrorBoundary)).toHaveLength(1);
expect(html).not.toMatch('support@readme.io');
expect(html).toMatch("endpoint's documentation");
});

describe('support-focused error messaging', () => {
it('should output with an error message if the endpoint fails to load, with a unique error event id', () => {
const doc = mount(<Doc {...props} doc={docProps} maskErrorMessages={false} oas={brokenOas} />);

doc.setState({ showEndpoint: true });

const html = doc.html();

expect(spy).toHaveBeenCalled();
expect(doc.find(ErrorBoundary)).toHaveLength(1);
expect(html).toMatch('support@readme.io');
expect(html).toMatch("endpoint's documentation");
});
});

it('should output with an error message if the endpoint fails to load, with a defined error event id', () => {
const doc = mount(
<Doc
{...props}
doc={docProps}
maskErrorMessages={false}
oas={brokenOas}
onError={() => {
return 'API-EXPLORER-1';
}}
/>
);

doc.setState({ showEndpoint: true });

expect(doc.find('EndpointErrorBoundary')).toHaveLength(1);
const html = doc.html();

expect(spy).toHaveBeenCalled();
expect(doc.find(ErrorBoundary)).toHaveLength(1);
expect(html).toMatch('support@readme.io');
expect(html).toMatch("endpoint's documentation");
expect(html).toMatch('API-EXPLORER-1');
});
});
42 changes: 37 additions & 5 deletions packages/api-explorer/__tests__/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ const React = require('react');
const { shallow, mount } = require('enzyme');
const Cookie = require('js-cookie');
const extensions = require('@readme/oas-extensions');
const shortid = require('shortid');
const WrappedApiExplorer = require('../src');
const ErrorBoundary = require('../src/ErrorBoundary');

const { ApiExplorer } = WrappedApiExplorer;

Expand Down Expand Up @@ -48,12 +50,42 @@ test('ApiExplorer should not render a common parameter OAS operation method', ()
expect(explorer.find('Doc')).toHaveLength(docsCommon.length - 2);
});

test('should display an error message if it fails to render (wrapped in ErrorBoundary)', () => {
// Prompting an error with an array of nulls instead of Docs
// This is to simulate some unknown error state during initial render
const explorer = mount(<WrappedApiExplorer {...props} docs={[null, null]} />);
describe('error handling', () => {
const spy = jest.spyOn(shortid, 'generate');
const originalConsole = console;

expect(explorer.find('ErrorBoundary')).toHaveLength(1);
// We're testing errors here, so we don't need `console.error` logs spamming the test output.
beforeEach(() => {
// eslint-disable-next-line no-console
console.error = () => {};
});

afterEach(() => {
// eslint-disable-next-line no-console
console.error = originalConsole.error;
});

it('should display a masked error message if it fails to render', () => {
const explorer = mount(<WrappedApiExplorer {...props} docs={[null, null]} maskErrorMessages={true} />);

const html = explorer.html();

expect(spy).toHaveBeenCalled();
expect(explorer.find(ErrorBoundary)).toHaveLength(1);
expect(html).not.toMatch('support@readme.io');
expect(html).toMatch('API Explorer');
});

it('should output a support-focused error message if it fails to render', () => {
const explorer = mount(<WrappedApiExplorer {...props} docs={[null, null]} maskErrorMessages={false} />);

const html = explorer.html();

expect(spy).toHaveBeenCalled();
expect(explorer.find(ErrorBoundary)).toHaveLength(1);
expect(html).toMatch('support@readme.io');
expect(html).toMatch('API Explorer');
});
});

describe('selected language', () => {
Expand Down
20 changes: 0 additions & 20 deletions packages/api-explorer/src/BoundaryStackTrace.jsx

This file was deleted.

12 changes: 8 additions & 4 deletions packages/api-explorer/src/Doc.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const createParams = require('./Params');
const CodeSample = require('./CodeSample');
const Response = require('./Response');
const ResponseSchema = require('./ResponseSchema');
const EndpointErrorBoundary = require('./EndpointErrorBoundary');
const ErrorBoundary = require('./ErrorBoundary');

const { Operation } = Oas;
const parseResponse = require('./lib/parse-response');
Expand Down Expand Up @@ -231,13 +231,13 @@ class Doc extends React.Component {
}

renderEndpoint() {
const { doc } = this.props;
const { doc, maskErrorMessages, onError } = this.props;
this.props.onDocRender(doc.slug);

return (
<EndpointErrorBoundary>
<ErrorBoundary appContext="endpoint" maskErrorMessages={maskErrorMessages} onError={onError}>
{this.props.appearance.referenceLayout === 'column' ? this.columnTheme(doc) : this.mainTheme(doc)}
</EndpointErrorBoundary>
</ErrorBoundary>
);
}

Expand Down Expand Up @@ -393,10 +393,12 @@ Doc.propTypes = {
language: PropTypes.string.isRequired,
lazy: PropTypes.bool,
Logs: PropTypes.func,
maskErrorMessages: PropTypes.bool,
oas: PropTypes.shape({}),
oauth: PropTypes.bool.isRequired,
onAuthChange: PropTypes.func.isRequired,
onDocRender: PropTypes.func.isRequired,
onError: PropTypes.func,
onGroupChange: PropTypes.func.isRequired,
rendered: PropTypes.bool,
setLanguage: PropTypes.func.isRequired,
Expand All @@ -418,7 +420,9 @@ Doc.defaultProps = {
groups: [],
lazy: true,
Logs: undefined,
maskErrorMessages: true,
oas: {},
onError: () => {},
rendered: false,
user: {},
};
Expand Down
Loading