2021-06-02 21:18:32 +02:00
|
|
|
jest.mock('./helpers');
|
|
|
|
jest.mock('./windowEvents');
|
|
|
|
jest.mock('./windowHelpers');
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
import { dialog, BrowserWindow, HandlerDetails, WebContents } from 'electron';
|
2021-06-26 15:59:28 +02:00
|
|
|
import { WindowOptions } from '../../../shared/src/options/model';
|
2021-06-02 21:18:32 +02:00
|
|
|
import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers';
|
2021-06-26 15:59:28 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
|
|
const {
|
|
|
|
onNewWindowHelper,
|
|
|
|
onWillNavigate,
|
|
|
|
onWillPreventUnload,
|
|
|
|
}: {
|
|
|
|
onNewWindowHelper: (
|
|
|
|
options: WindowOptions,
|
|
|
|
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
2023-08-25 15:10:05 +02:00
|
|
|
details: Partial<HandlerDetails>,
|
2021-06-26 15:59:28 +02:00
|
|
|
parent?: BrowserWindow,
|
2023-08-25 15:10:05 +02:00
|
|
|
) => ReturnType<Parameters<WebContents['setWindowOpenHandler']>[0]>;
|
2021-06-26 15:59:28 +02:00
|
|
|
onWillNavigate: (
|
|
|
|
options: {
|
|
|
|
blockExternalUrls: boolean;
|
|
|
|
internalUrls?: string | RegExp;
|
|
|
|
targetUrl: string;
|
|
|
|
},
|
|
|
|
event: unknown,
|
|
|
|
urlToGo: string,
|
|
|
|
) => Promise<void>;
|
|
|
|
onWillPreventUnload: (event: unknown) => void;
|
|
|
|
} = jest.requireActual('./windowEvents');
|
2021-06-02 21:18:32 +02:00
|
|
|
import {
|
2022-09-18 04:22:19 +02:00
|
|
|
showNavigationBlockedMessage,
|
2021-06-02 21:18:32 +02:00
|
|
|
createAboutBlankWindow,
|
|
|
|
createNewTab,
|
|
|
|
} from './windowHelpers';
|
|
|
|
|
|
|
|
describe('onNewWindowHelper', () => {
|
|
|
|
const originalURL = 'https://medium.com/';
|
|
|
|
const internalURL = 'https://medium.com/topics/technology';
|
|
|
|
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
|
|
|
|
const foregroundDisposition = 'foreground-tab';
|
|
|
|
const backgroundDisposition = 'background-tab';
|
2021-06-26 15:59:28 +02:00
|
|
|
const baseOptions = {
|
2023-08-25 15:10:05 +02:00
|
|
|
autoHideMenuBar: true,
|
2021-06-26 15:59:28 +02:00
|
|
|
blockExternalUrls: false,
|
|
|
|
insecure: false,
|
|
|
|
name: 'TEST_APP',
|
|
|
|
targetUrl: originalURL,
|
|
|
|
zoom: 1.0,
|
2023-08-25 15:10:05 +02:00
|
|
|
} as WindowOptions;
|
2022-09-18 04:22:19 +02:00
|
|
|
const mockShowNavigationBlockedMessage: jest.SpyInstance =
|
|
|
|
showNavigationBlockedMessage as jest.Mock;
|
2021-06-02 21:18:32 +02:00
|
|
|
const mockCreateAboutBlank: jest.SpyInstance =
|
|
|
|
createAboutBlankWindow as jest.Mock;
|
|
|
|
const mockCreateNewTab: jest.SpyInstance = createNewTab as jest.Mock;
|
|
|
|
const mockLinkIsInternal: jest.SpyInstance = (
|
|
|
|
linkIsInternal as jest.Mock
|
|
|
|
).mockImplementation(() => true);
|
|
|
|
const mockNativeTabsSupported: jest.SpyInstance =
|
|
|
|
nativeTabsSupported as jest.Mock;
|
|
|
|
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
|
|
|
|
const setupWindow = jest.fn();
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2022-09-18 04:22:19 +02:00
|
|
|
mockShowNavigationBlockedMessage
|
2021-06-02 21:18:32 +02:00
|
|
|
.mockReset()
|
|
|
|
.mockReturnValue(Promise.resolve(undefined));
|
|
|
|
mockCreateAboutBlank.mockReset();
|
|
|
|
mockCreateNewTab.mockReset();
|
|
|
|
mockLinkIsInternal.mockReset().mockReturnValue(true);
|
|
|
|
mockNativeTabsSupported.mockReset().mockReturnValue(false);
|
|
|
|
mockOpenExternal.mockReset();
|
|
|
|
setupWindow.mockReset();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(() => {
|
2022-09-18 04:22:19 +02:00
|
|
|
mockShowNavigationBlockedMessage.mockRestore();
|
2021-06-02 21:18:32 +02:00
|
|
|
mockCreateAboutBlank.mockRestore();
|
|
|
|
mockCreateNewTab.mockRestore();
|
|
|
|
mockLinkIsInternal.mockRestore();
|
|
|
|
mockNativeTabsSupported.mockRestore();
|
|
|
|
mockOpenExternal.mockRestore();
|
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('internal urls should not be handled', () => {
|
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: internalURL,
|
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
|
|
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('allow');
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('external urls should be opened externally', () => {
|
2021-06-02 21:18:32 +02:00
|
|
|
mockLinkIsInternal.mockReturnValue(false);
|
2021-06-26 15:59:28 +02:00
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: externalURL,
|
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
|
|
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('deny');
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('external urls should be ignored if blockExternalUrls is true', () => {
|
2021-06-02 21:18:32 +02:00
|
|
|
mockLinkIsInternal.mockReturnValue(false);
|
|
|
|
const options = {
|
2021-06-26 15:59:28 +02:00
|
|
|
...baseOptions,
|
2021-06-02 21:18:32 +02:00
|
|
|
blockExternalUrls: true,
|
|
|
|
};
|
2023-08-25 15:10:05 +02:00
|
|
|
const result = onNewWindowHelper(options, setupWindow, {
|
|
|
|
url: externalURL,
|
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
|
|
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('deny');
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('tab disposition should be ignored if tabs are not enabled', () => {
|
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: internalURL,
|
|
|
|
disposition: foregroundDisposition,
|
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
|
|
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('allow');
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('tab disposition should be ignored if url is external', () => {
|
2021-06-02 21:18:32 +02:00
|
|
|
mockLinkIsInternal.mockReturnValue(false);
|
2021-06-26 15:59:28 +02:00
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: externalURL,
|
|
|
|
disposition: foregroundDisposition,
|
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
|
|
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('deny');
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('foreground tabs with internal urls should be opened in the foreground', () => {
|
2021-06-02 21:18:32 +02:00
|
|
|
mockNativeTabsSupported.mockReturnValue(true);
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: internalURL,
|
|
|
|
disposition: foregroundDisposition,
|
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
|
|
|
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
|
|
|
expect(mockCreateNewTab).toHaveBeenCalledWith(
|
2021-06-26 15:59:28 +02:00
|
|
|
baseOptions,
|
2021-06-02 21:18:32 +02:00
|
|
|
setupWindow,
|
|
|
|
internalURL,
|
|
|
|
true,
|
|
|
|
);
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('deny');
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('background tabs with internal urls should be opened in background tabs', () => {
|
2021-06-02 21:18:32 +02:00
|
|
|
mockNativeTabsSupported.mockReturnValue(true);
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: internalURL,
|
|
|
|
disposition: backgroundDisposition,
|
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
|
|
|
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
|
|
|
expect(mockCreateNewTab).toHaveBeenCalledWith(
|
2021-06-26 15:59:28 +02:00
|
|
|
baseOptions,
|
2021-06-02 21:18:32 +02:00
|
|
|
setupWindow,
|
|
|
|
internalURL,
|
|
|
|
false,
|
|
|
|
);
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('deny');
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('about:blank urls should be handled', () => {
|
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: 'about:blank',
|
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
|
|
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('deny');
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
2021-06-17 05:03:49 +02:00
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('about:blank#blocked urls should be handled', () => {
|
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: 'about:blank#blocked',
|
|
|
|
});
|
2021-06-17 05:03:49 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
|
|
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-17 05:03:49 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('deny');
|
2021-06-17 05:03:49 +02:00
|
|
|
});
|
|
|
|
|
2023-08-25 15:10:05 +02:00
|
|
|
test('about:blank#other urls should not be handled', () => {
|
|
|
|
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
|
|
|
url: 'about:blank#other',
|
|
|
|
});
|
2021-06-17 05:03:49 +02:00
|
|
|
|
|
|
|
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
|
|
|
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-17 05:03:49 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
2023-08-25 15:10:05 +02:00
|
|
|
expect(result.action).toEqual('allow');
|
2021-06-17 05:03:49 +02:00
|
|
|
});
|
2021-06-02 21:18:32 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('onWillNavigate', () => {
|
|
|
|
const originalURL = 'https://medium.com/';
|
|
|
|
const internalURL = 'https://medium.com/topics/technology';
|
|
|
|
const externalURL = 'https://www.wikipedia.org/wiki/Electron';
|
|
|
|
|
2022-09-18 04:22:19 +02:00
|
|
|
const mockShowNavigationBlockedMessage: jest.SpyInstance =
|
|
|
|
showNavigationBlockedMessage as jest.Mock;
|
2021-06-02 21:18:32 +02:00
|
|
|
const mockLinkIsInternal: jest.SpyInstance = linkIsInternal as jest.Mock;
|
|
|
|
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
|
|
|
|
const preventDefault = jest.fn();
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2022-09-18 04:22:19 +02:00
|
|
|
mockShowNavigationBlockedMessage
|
2021-06-02 21:18:32 +02:00
|
|
|
.mockReset()
|
|
|
|
.mockReturnValue(Promise.resolve(undefined));
|
|
|
|
mockLinkIsInternal.mockReset().mockReturnValue(false);
|
|
|
|
mockOpenExternal.mockReset();
|
|
|
|
preventDefault.mockReset();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(() => {
|
2022-09-18 04:22:19 +02:00
|
|
|
mockShowNavigationBlockedMessage.mockRestore();
|
2021-06-02 21:18:32 +02:00
|
|
|
mockLinkIsInternal.mockRestore();
|
|
|
|
mockOpenExternal.mockRestore();
|
|
|
|
});
|
|
|
|
|
2021-06-26 15:59:28 +02:00
|
|
|
test('internal urls should not be handled', async () => {
|
2021-06-02 21:18:32 +02:00
|
|
|
mockLinkIsInternal.mockReturnValue(true);
|
|
|
|
const options = {
|
|
|
|
blockExternalUrls: false,
|
|
|
|
targetUrl: originalURL,
|
|
|
|
};
|
|
|
|
const event = { preventDefault };
|
2021-06-26 15:59:28 +02:00
|
|
|
await onWillNavigate(options, event, internalURL);
|
2021-06-02 21:18:32 +02:00
|
|
|
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
|
|
|
expect(preventDefault).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2021-06-26 15:59:28 +02:00
|
|
|
test('external urls should be opened externally', async () => {
|
2021-06-02 21:18:32 +02:00
|
|
|
const options = {
|
|
|
|
blockExternalUrls: false,
|
|
|
|
targetUrl: originalURL,
|
|
|
|
};
|
|
|
|
const event = { preventDefault };
|
2021-06-26 15:59:28 +02:00
|
|
|
await onWillNavigate(options, event, externalURL);
|
2021-06-02 21:18:32 +02:00
|
|
|
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
|
|
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
|
|
|
});
|
|
|
|
|
2022-09-18 04:22:19 +02:00
|
|
|
test('external urls should be blocked if blockExternalUrls is true', async () => {
|
2021-06-02 21:18:32 +02:00
|
|
|
const options = {
|
|
|
|
blockExternalUrls: true,
|
|
|
|
targetUrl: originalURL,
|
|
|
|
};
|
|
|
|
const event = { preventDefault };
|
2021-06-26 15:59:28 +02:00
|
|
|
await onWillNavigate(options, event, externalURL);
|
2021-06-02 21:18:32 +02:00
|
|
|
|
2022-09-18 04:22:19 +02:00
|
|
|
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
|
2021-06-02 21:18:32 +02:00
|
|
|
expect(mockOpenExternal).not.toHaveBeenCalled();
|
|
|
|
expect(preventDefault).toHaveBeenCalledTimes(1);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('onWillPreventUnload', () => {
|
|
|
|
const mockFromWebContents: jest.SpyInstance = jest
|
|
|
|
.spyOn(BrowserWindow, 'fromWebContents')
|
|
|
|
.mockImplementation(() => new BrowserWindow());
|
|
|
|
const mockShowDialog: jest.SpyInstance = jest.spyOn(
|
|
|
|
dialog,
|
|
|
|
'showMessageBoxSync',
|
|
|
|
);
|
|
|
|
const preventDefault: jest.SpyInstance = jest.fn();
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
mockFromWebContents.mockReset();
|
|
|
|
mockShowDialog.mockReset().mockReturnValue(undefined);
|
|
|
|
preventDefault.mockReset();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterAll(() => {
|
|
|
|
mockFromWebContents.mockRestore();
|
|
|
|
mockShowDialog.mockRestore();
|
|
|
|
});
|
|
|
|
|
|
|
|
test('with no sender', () => {
|
|
|
|
const event = {};
|
|
|
|
onWillPreventUnload(event);
|
|
|
|
|
|
|
|
expect(mockFromWebContents).not.toHaveBeenCalled();
|
|
|
|
expect(mockShowDialog).not.toHaveBeenCalled();
|
|
|
|
expect(preventDefault).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
test('shows dialog and calls preventDefault on ok', () => {
|
|
|
|
mockShowDialog.mockReturnValue(0);
|
|
|
|
|
2022-01-02 12:17:06 +01:00
|
|
|
const event = { preventDefault, sender: {} };
|
2021-06-02 21:18:32 +02:00
|
|
|
onWillPreventUnload(event);
|
|
|
|
|
|
|
|
expect(mockFromWebContents).toHaveBeenCalledWith(event.sender);
|
|
|
|
expect(mockShowDialog).toHaveBeenCalled();
|
|
|
|
expect(preventDefault).toHaveBeenCalledWith();
|
|
|
|
});
|
|
|
|
|
|
|
|
test('shows dialog and does not call preventDefault on cancel', () => {
|
|
|
|
mockShowDialog.mockReturnValue(1);
|
|
|
|
|
2022-01-02 12:17:06 +01:00
|
|
|
const event = { preventDefault, sender: {} };
|
2021-06-02 21:18:32 +02:00
|
|
|
onWillPreventUnload(event);
|
|
|
|
|
|
|
|
expect(mockFromWebContents).toHaveBeenCalledWith(event.sender);
|
|
|
|
expect(mockShowDialog).toHaveBeenCalled();
|
|
|
|
expect(preventDefault).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|