Skip to content

Commit

Permalink
Make mock WebChannel less invasive and add a cleanup function.
Browse files Browse the repository at this point in the history
The WebChannel mocking now calls the original functions for
addEventListener, removeEventListener, and dispatchEvent. This means that
it won't interfere with other uses of these APIs.

On example of another use is the popstate event: UrlManager.test.js has
a test which makes sure that the URL state is restored when going
backwards in history. Using a mocked WebChannel in that test broke it
because the popstate event was no longer being dispatched.

Furthermore, this patch adds "restoreOriginals()" calls to all users of
the mock WebChannel. I haven't seen any problems from not restoring the
un-mocked functions, but it seems like the right thing to do.
  • Loading branch information
mstange committed Nov 9, 2021
1 parent f0f9481 commit d0af54c
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 45 deletions.
9 changes: 7 additions & 2 deletions src/test/components/DragAndDrop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,11 @@ describe('app/DragAndDrop', () => {
// When the file is dropped, the profiler tries to connect to the WebChannel
// for symbolication. Handle that request so that we don't time out.
// We handle it by rejecting it.
const { registerMessageToChromeListener, triggerResponse } =
mockWebChannel();
const {
registerMessageToChromeListener,
triggerResponse,
restoreOriginals,
} = mockWebChannel();
registerMessageToChromeListener(() => {
triggerResponse({
errno: 2, // ERRNO_NO_SUCH_CHANNEL
Expand Down Expand Up @@ -124,5 +127,7 @@ describe('app/DragAndDrop', () => {

fireEvent.dragLeave(dragAndDrop);
expect(overlay.classList).not.toContain('dragging');

restoreOriginals();
});
});
9 changes: 7 additions & 2 deletions src/test/components/Home.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ describe('app/Home', function () {
// This test's assertions are that it can find elements through getByTestId.
// eslint-disable-next-line jest/expect-expect
it('will switch to recording instructions when enabling the popup', async () => {
const { triggerResponse, getLastRequestId } = mockWebChannel();
const { triggerResponse, getLastRequestId, restoreOriginals } =
mockWebChannel();
const { findByTestId, getByText } = setup(FIREFOX);

// Respond back from the browser that the menu button is not yet enabled.
Expand All @@ -78,14 +79,16 @@ describe('app/Home', function () {
response: undefined,
});
await findByTestId('home-record-instructions');

restoreOriginals();
});

it('renders an error if the WebChannel is not available', async () => {
// This simulates what happens if the profiler is run from a host which
// is not the configured profiler base-url, or in an old Firefox version (<76)
// which has WebChannels but no profiler WebChannel.

const { listeners, triggerResponse } = mockWebChannel();
const { listeners, triggerResponse, restoreOriginals } = mockWebChannel();

// No one has asked anything to the WebChannel.
expect(listeners).toHaveLength(0);
Expand All @@ -110,5 +113,7 @@ describe('app/Home', function () {
);

expect(webChannelUnavailableMessage).toMatchSnapshot();

restoreOriginals();
});
});
44 changes: 30 additions & 14 deletions src/test/fixtures/mocks/web-channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,47 @@ import type {
export function mockWebChannel() {
const messagesSentToBrowser = [];
const listeners = [];
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
const originalDispatchEvent = window.dispatchEvent;
let onMessageToChrome = null;

jest
const spyAddEventListener = jest
.spyOn(window, 'addEventListener')
.mockImplementation((name, listener) => {
.mockImplementation((name, listener, options) => {
if (name === 'WebChannelMessageToContent') {
listeners.push(listener);
}
originalAddEventListener.call(window, name, listener, options);
});

jest
const spyRemoveEventListener = jest
.spyOn(window, 'removeEventListener')
.mockImplementation((name, listener) => {
.mockImplementation((name, listener, options) => {
if (name === 'WebChannelMessageToContent') {
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
}
originalRemoveEventListener.call(window, name, listener, options);
});

jest.spyOn(window, 'dispatchEvent').mockImplementation((event) => {
if (
event instanceof CustomEvent &&
event.type === 'WebChannelMessageToChrome'
) {
messagesSentToBrowser.push(JSON.parse(event.detail));
if (onMessageToChrome) {
onMessageToChrome(JSON.parse(event.detail).message);
const spyDispatchEvent = jest
.spyOn(window, 'dispatchEvent')
.mockImplementation((event) => {
if (
event instanceof CustomEvent &&
event.type === 'WebChannelMessageToChrome'
) {
messagesSentToBrowser.push(JSON.parse(event.detail));
if (onMessageToChrome) {
onMessageToChrome(JSON.parse(event.detail).message);
}
} else {
originalDispatchEvent.call(window, event);
}
}
});
});

function triggerResponse<R: ResponseFromBrowser>(
message: MessageFromBrowser<R>
Expand All @@ -68,6 +77,12 @@ export function mockWebChannel() {
onMessageToChrome = listener;
}

function restoreOriginals() {
spyAddEventListener.mockRestore();
spyRemoveEventListener.mockRestore();
spyDispatchEvent.mockRestore();
}

return {
messagesSentToBrowser,
listeners,
Expand All @@ -84,6 +99,7 @@ export function mockWebChannel() {
}
return requestId;
},
restoreOriginals,
};
}

Expand Down
73 changes: 50 additions & 23 deletions src/test/store/receive-profile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ describe('actions/receive-profile', function () {
'frame-script': setupWithFrameScript,
'web-channel': setupWithWebChannel,
}[setupWith];
const { dispatch, getState } = setupFn(profileAs);
const { dispatch, getState, restoreOriginals } = setupFn(profileAs);
await dispatch(retrieveProfileFromBrowser());
expect(console.warn).toHaveBeenCalledTimes(2);

Expand All @@ -779,12 +779,14 @@ describe('actions/receive-profile', function () {
expect(ProfileViewSelectors.getProfile(state).threads).toHaveLength(
3
);
restoreOriginals();
});
}
}

it('tries to symbolicate the received profile, frame script version', async () => {
const { dispatch, geckoProfiler } = setupWithFrameScript();
const { dispatch, geckoProfiler, restoreOriginals } =
setupWithFrameScript();

await dispatch(retrieveProfileFromBrowser());

Expand All @@ -799,10 +801,12 @@ describe('actions/receive-profile', function () {
body: expect.stringMatching(/memoryMap.*firefox/),
})
);

restoreOriginals();
});

it('tries to symbolicate the received profile, webchannel version', async () => {
const { dispatch } = setupWithWebChannel();
const { dispatch, restoreOriginals } = setupWithWebChannel();

await dispatch(retrieveProfileFromBrowser());

Expand All @@ -812,6 +816,8 @@ describe('actions/receive-profile', function () {
body: expect.stringMatching(/memoryMap.*firefox/),
})
);

restoreOriginals();
});
});

Expand Down Expand Up @@ -1395,8 +1401,11 @@ describe('actions/receive-profile', function () {
// When the file is loaded, the profiler tries to connect to the WebChannel
// for symbolication. Handle that request so that we don't time out.
// We handle it by rejecting it.
const { registerMessageToChromeListener, triggerResponse } =
mockWebChannel();
const {
registerMessageToChromeListener,
triggerResponse,
restoreOriginals,
} = mockWebChannel();
registerMessageToChromeListener(() => {
triggerResponse({
errno: 2, // ERRNO_NO_SUCH_CHANNEL
Expand All @@ -1411,21 +1420,23 @@ describe('actions/receive-profile', function () {
const { dispatch, getState } = blankStore();
await dispatch(retrieveProfileFromFile(file, mockFileReader));
const view = getView(getState());
return { getState, dispatch, view };
return { getState, dispatch, view, restoreOriginals };
}

it('can load json with a good mime type', async function () {
const profile = _getSimpleProfile();
profile.meta.product = 'JSON Test';

const { getState, view } = await setupTestWithFile({
const { getState, view, restoreOriginals } = await setupTestWithFile({
type: 'application/json',
payload: serializeProfile(profile),
});
expect(view.phase).toBe('DATA_LOADED');
expect(ProfileViewSelectors.getProfile(getState()).meta.product).toEqual(
'JSON Test'
);

restoreOriginals();
});

it('symbolicates unsymbolicated profiles', async function () {
Expand Down Expand Up @@ -2027,7 +2038,7 @@ describe('actions/receive-profile', function () {
.mockRejectedValue(new Error('No symbol tables available')),
};

simulateOldWebChannelAndFrameScript(geckoProfiler);
const webChannel = simulateOldWebChannelAndFrameScript(geckoProfiler);

simulateSymbolStoreHasNoCache();

Expand Down Expand Up @@ -2059,6 +2070,7 @@ describe('actions/receive-profile', function () {
geckoProfile,
waitUntilPhase,
waitUntilSymbolication,
webChannel,
...store,
};
}
Expand All @@ -2076,7 +2088,7 @@ describe('actions/receive-profile', function () {
});

it('retrieves profile from a `public` data source and loads it', async function () {
const { profile, getState, dispatch } = await setup({
const { profile, getState, dispatch, webChannel } = await setup({
pathname: '/public/fakehash/',
search: '?thread=0&v=4',
hash: '',
Expand All @@ -2089,10 +2101,12 @@ describe('actions/receive-profile', function () {
// Check if we can successfully finalize the profile view.
await dispatch(finalizeProfileView());
expect(getView(getState()).phase).toBe('DATA_LOADED');

webChannel.restoreOriginals();
});

it('retrieves profile from a `from-url` data source and loads it', async function () {
const { profile, getState, dispatch } = await setup({
const { profile, getState, dispatch, webChannel } = await setup({
// '/from-url/https://fakeurl.com/fakeprofile.json/'
pathname: '/from-url/https%3A%2F%2Ffakeurl.com%2Ffakeprofile.json/',
search: '',
Expand All @@ -2106,10 +2120,12 @@ describe('actions/receive-profile', function () {
// Check if we can successfully finalize the profile view.
await dispatch(finalizeProfileView());
expect(getView(getState()).phase).toBe('DATA_LOADED');

webChannel.restoreOriginals();
});

it('keeps the `from-url` value in the URL', async function () {
const { getState, dispatch } = await setup({
const { getState, dispatch, webChannel } = await setup({
// '/from-url/https://fakeurl.com/fakeprofile.json/'
pathname: '/from-url/https%3A%2F%2Ffakeurl.com%2Ffakeprofile.json/',
search: '',
Expand All @@ -2121,12 +2137,14 @@ describe('actions/receive-profile', function () {
).split('/');
expect(fromUrl).toEqual('from-url');
expect(urlString).toEqual('https%3A%2F%2Ffakeurl.com%2Ffakeprofile.json');

webChannel.restoreOriginals();
});

it('retrieves profile from a `compare` data source and loads it', async function () {
const url1 = 'http://fake-url.com/public/1?thread=0';
const url2 = 'http://fake-url.com/public/2?thread=0';
const { getState, dispatch } = await setup(
const { getState, dispatch, webChannel } = await setup(
{
pathname: '/compare/FAKEURL/',
search: oneLineTrim`
Expand All @@ -2144,17 +2162,20 @@ describe('actions/receive-profile', function () {
// Check if we can successfully finalize the profile view.
await dispatch(finalizeProfileView());
expect(getView(getState()).phase).toBe('DATA_LOADED');

webChannel.restoreOriginals();
});

it('retrieves profile from a `from-browser` data source and loads it', async function () {
const { geckoProfile, getState, waitUntilPhase } = await setup(
{
pathname: '/from-browser/',
search: '',
hash: '',
},
0
);
const { geckoProfile, getState, waitUntilPhase, webChannel } =
await setup(
{
pathname: '/from-browser/',
search: '',
hash: '',
},
0
);

// Differently, `from-browser` calls the finalizeProfileView internally,
// we don't need to call it again.
Expand All @@ -2163,10 +2184,12 @@ describe('actions/receive-profile', function () {
expect(ProfileViewSelectors.getProfile(getState())).toEqual(
processedProfile
);

webChannel.restoreOriginals();
});

it('finishes symbolication for `from-browser` data source', async function () {
const { waitUntilSymbolication } = await setup(
const { waitUntilSymbolication, webChannel } = await setup(
{
pathname: '/from-browser/',
search: '',
Expand All @@ -2176,21 +2199,25 @@ describe('actions/receive-profile', function () {
);

// It should successfully symbolicate the profiles that are loaded from the browser.
return expect(waitUntilSymbolication()).resolves.toBe(undefined);
await expect(waitUntilSymbolication()).resolves.toBe(undefined);

webChannel.restoreOriginals();
});

['none', 'from-file', 'local', 'uploaded-recordings', 'compare'].forEach(
(dataSource) => {
it(`does not retrieve a profile for the datasource ${dataSource}`, async function () {
const sourcePath = `/${dataSource}/`;
const { getState } = await setup({
const { getState, webChannel } = await setup({
pathname: sourcePath,
search: '',
hash: '',
});
expect(ProfileViewSelectors.getProfileOrNull(getState())).toEqual(
null
);

webChannel.restoreOriginals();
});
}
);
Expand Down
Loading

0 comments on commit d0af54c

Please sign in to comment.