Skip to content

Commit 83dc705

Browse files
authored
feat: Error boundary default refresh always takes top window (#4224)
1 parent e494891 commit 83dc705

File tree

3 files changed

+56
-13
lines changed

3 files changed

+56
-13
lines changed

src/error-boundary/__tests__/error-boundary.test.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,9 @@ import { render } from '@testing-library/react';
77
import ErrorBoundary, { ErrorBoundaryProps } from '../../../lib/components/error-boundary';
88
import { BuiltInErrorBoundaryProps } from '../../../lib/components/error-boundary/interfaces';
99
import { BuiltInErrorBoundary } from '../../../lib/components/error-boundary/internal';
10-
import { refreshPage } from '../../../lib/components/error-boundary/utils';
1110
import TestI18nProvider from '../../../lib/components/i18n/testing';
1211
import createWrapper from '../../../lib/components/test-utils/dom';
1312

14-
jest.mock('../../../lib/components/error-boundary/utils', () => ({
15-
...jest.requireActual('../../../lib/components/error-boundary/utils'),
16-
refreshPage: jest.fn(),
17-
}));
18-
1913
type RenderProps = Omit<
2014
Partial<ErrorBoundaryProps> & { i18nProvider?: Record<string, Record<string, string>> } & {
2115
[key: `data-${string}`]: string;
@@ -618,10 +612,39 @@ describe('built-in error boundaries', () => {
618612
});
619613

620614
describe('default behaviors', () => {
615+
let originalLocation: PropertyDescriptor | undefined;
616+
617+
beforeEach(() => {
618+
originalLocation = Object.getOwnPropertyDescriptor(window, 'location');
619+
});
620+
621+
afterEach(() => {
622+
if (originalLocation) {
623+
Object.defineProperty(window, 'location', originalLocation);
624+
}
625+
});
626+
621627
test('window reload is called when the refresh action is clicked', () => {
628+
const mockReload = jest.fn();
629+
Object.defineProperty(window, 'location', { configurable: true, value: { reload: mockReload } });
630+
622631
renderWithErrorBoundary(<b>{{}}</b>);
623632
findRefreshAction()!.click();
624-
expect(refreshPage).toHaveBeenCalledTimes(1);
633+
expect(mockReload).toHaveBeenCalledTimes(1);
634+
});
635+
636+
test('hides default refresh in cross-origin iframes', () => {
637+
Object.defineProperty(window, 'location', {
638+
configurable: true,
639+
value: {
640+
get href() {
641+
throw new Error();
642+
},
643+
},
644+
});
645+
646+
renderWithErrorBoundary(<b>{{}}</b>);
647+
expect(findRefreshAction()).toBe(null);
625648
});
626649
});
627650

src/error-boundary/fallback.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import InternalButton from '../button/internal';
1010
import { useInternalI18n } from '../i18n/context';
1111
import { getBaseProps } from '../internal/base-component';
1212
import { ErrorBoundaryProps } from './interfaces';
13-
import { refreshPage } from './utils';
13+
import { canUseRefresh, refreshPage } from './utils';
1414

1515
import styles from './styles.css.js';
1616
import testUtilStyles from './test-classes/styles.css.js';
@@ -32,11 +32,11 @@ export function ErrorBoundaryFallback({
3232
<DefaultDescriptionContent i18nStrings={i18nStrings} />
3333
</div>
3434
),
35-
action: (
35+
action: canUseRefresh() ? (
3636
<div className={clsx(styles.action, testUtilStyles.action)}>
3737
<DefaultActionContent i18nStrings={i18nStrings} />
3838
</div>
39-
),
39+
) : null,
4040
};
4141
return (
4242
<div {...baseProps} className={clsx(baseProps.className, testUtilStyles.fallback)}>

src/error-boundary/utils.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
/* istanbul ignore next */
5-
export function refreshPage() {
6-
window.location.reload();
4+
// The default refresh action reloads the top window to avoid iframe lifecycle getting out of sync with the parent page.
5+
// This is not possible from cross-origin iframes, in which case the default refresh action must be hidden.
6+
export function canUseRefresh(): boolean {
7+
try {
8+
// In cross-origin iframes, accessing top.location can throw a SecurityError.
9+
void getTopWindow().location.href;
10+
return true;
11+
} catch {
12+
return false;
13+
}
14+
}
15+
16+
export function refreshPage(): void {
17+
try {
18+
getTopWindow().location.reload();
19+
} catch {
20+
// noop
21+
}
22+
}
23+
24+
// In browsers, window.top is always defined, but it is treated as optional by our current DOM types.
25+
function getTopWindow(): Window {
26+
return window.top ?? window;
727
}

0 commit comments

Comments
 (0)