Skip to content

Commit

Permalink
Add tests for the new initial url/profile loading mechanism (Merge PR f…
Browse files Browse the repository at this point in the history
  • Loading branch information
canova authored Jul 19, 2019
2 parents f68bcc8 + c1fca76 commit 3067dda
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 16 deletions.
8 changes: 5 additions & 3 deletions src/actions/receive-profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
initializeHiddenGlobalTracks,
getVisibleThreads,
} from '../profile-logic/tracks';
import { getProfile, getProfileOrNull } from '../selectors/profile';
import { getProfileOrNull } from '../selectors/profile';
import { getView } from '../selectors/app';
import { setDataSource } from './profile-view';

Expand Down Expand Up @@ -997,7 +997,7 @@ export function retrieveProfilesToCompare(
export function getProfilesFromRawUrl(
location: Location
): ThunkAction<
Promise<{| profile: Profile, shouldSetupInitialUrlState: boolean |}>
Promise<{| profile: Profile | null, shouldSetupInitialUrlState: boolean |}>
> {
return async (dispatch, getState) => {
const pathParts = location.pathname.split('/').filter(d => d);
Expand Down Expand Up @@ -1048,8 +1048,10 @@ export function getProfilesFromRawUrl(
);
}

// Profile may be null only for the `from-addon` dataSource since we do
// not `await` for retrieveProfileFromAddon function.
return {
profile: getProfile(getState()),
profile: getProfileOrNull(getState()),
shouldSetupInitialUrlState,
};
};
Expand Down
8 changes: 5 additions & 3 deletions src/components/app/UrlManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,17 @@ class UrlManager extends React.PureComponent<Props> {
try {
// Process the raw url and fetch the profile.
const results: {
profile: Profile,
profile: Profile | null,
shouldSetupInitialUrlState: boolean,
} = await getProfilesFromRawUrl(window.location);

// Manually coerce these into the proper type due to the FlowFixMe above.
const profile: Profile = results.profile;
// Profile may be null only for the `from-addon` dataSource since we do
// not `await` for retrieveProfileFromAddon function.
const profile: Profile | null = results.profile;
const shouldSetupInitialUrlState: boolean =
results.shouldSetupInitialUrlState;
if (shouldSetupInitialUrlState) {
if (profile !== null && shouldSetupInitialUrlState) {
setupInitialUrlState(window.location, profile);
} else {
urlSetupDone();
Expand Down
75 changes: 66 additions & 9 deletions src/test/components/UrlManager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ import { getUrlSetupPhase } from '../../selectors/app';
import UrlManager from '../../components/app/UrlManager';
import { blankStore } from '../fixtures/stores';
import { getDataSource } from '../../selectors/url-state';
import { waitUntilState } from '../fixtures/utils';
import { createGeckoProfile } from '../fixtures/profiles/gecko-profile';
import * as receiveProfile from '../../actions/receive-profile';

jest.mock('../../profile-logic/symbol-store');

describe('UrlManager', function() {
function setup(urlPath: ?string) {
(receiveProfile: any).doSymbolicateProfile = jest.fn(() => async () => {});

if (typeof urlPath === 'string') {
// jsdom doesn't allow us to rewrite window.location. Instead, use the
// History API to properly set the current location.
Expand All @@ -28,33 +35,83 @@ describe('UrlManager', function() {
<UrlManager>Contents</UrlManager>
</Provider>
);
return { dispatch, getState, createUrlManager };

const waitUntilUrlSetupPhase = phase =>
waitUntilState(store, state => getUrlSetupPhase(state) === phase);

return { dispatch, getState, createUrlManager, waitUntilUrlSetupPhase };
}

it('sets up the URL', function() {
const { getState, createUrlManager } = setup();
beforeEach(function() {
const profileJSON = createGeckoProfile();
const mockGetProfile = jest.fn().mockResolvedValue(profileJSON);

const geckoProfiler = {
getProfile: mockGetProfile,
getSymbolTable: jest
.fn()
.mockRejectedValue(new Error('No symbol tables available')),
};
window.fetch = jest
.fn()
.mockRejectedValue(new Error('No symbolication API in place'));
window.geckoProfilerPromise = Promise.resolve(geckoProfiler);
});

afterEach(function() {
delete window.geckoProfilerPromise;
delete window.fetch;
});

it('sets up the URL', async function() {
const { getState, createUrlManager, waitUntilUrlSetupPhase } = setup();
expect(getUrlSetupPhase(getState())).toBe('initial-load');
createUrlManager();

expect(getUrlSetupPhase(getState())).toBe('loading-profile');

await waitUntilUrlSetupPhase('done');
expect(getUrlSetupPhase(getState())).toBe('done');
expect(getDataSource(getState())).toMatch('none');
});

it('has no data source by default', function() {
const { getState, createUrlManager } = setup();
it('has no data source by default', async function() {
const { getState, createUrlManager, waitUntilUrlSetupPhase } = setup();
createUrlManager();
await waitUntilUrlSetupPhase('done');
expect(getDataSource(getState())).toMatch('none');
});

it('sets the data source to from-addon', function() {
const { getState, createUrlManager } = setup('/from-addon/');
it('sets the data source to from-addon', async function() {
const { getState, createUrlManager, waitUntilUrlSetupPhase } = setup(
'/from-addon/'
);
expect(getDataSource(getState())).toMatch('none');
createUrlManager();

await waitUntilUrlSetupPhase('done');
expect(getDataSource(getState())).toMatch('from-addon');
});

it('redirects from-file back to no data source', function() {
const { getState, createUrlManager } = setup('/from-file/');
it('redirects from-file back to no data source', async function() {
const { getState, createUrlManager, waitUntilUrlSetupPhase } = setup(
'/from-file/'
);
expect(getDataSource(getState())).toMatch('none');
createUrlManager();

await waitUntilUrlSetupPhase('done');
expect(getDataSource(getState())).toMatch('none');
});

it('sets the data source to public', async function() {
const { getState, createUrlManager, waitUntilUrlSetupPhase } = setup(
'/public/'
);
expect(getDataSource(getState())).toMatch('none');
createUrlManager();

await waitUntilUrlSetupPhase('done');
expect(getDataSource(getState())).toMatch('public');
});
});
191 changes: 190 additions & 1 deletion src/test/store/receive-profile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { Profile } from '../../types/profile';

import sinon from 'sinon';
import { oneLineTrim } from 'common-tags';

import { getEmptyProfile } from '../../profile-logic/data-structures';
import { viewProfileFromPathInZipFile } from '../../actions/zipped-profiles';
Expand All @@ -17,23 +18,29 @@ import { getThreadSelectors } from '../../selectors/per-thread';
import { getView } from '../../selectors/app';
import {
viewProfile,
finalizeProfileView,
retrieveProfileFromAddon,
retrieveProfileFromStore,
retrieveProfileOrZipFromUrl,
retrieveProfileFromFile,
retrieveProfilesToCompare,
_fetchProfile,
getProfilesFromRawUrl,
} from '../../actions/receive-profile';
import { SymbolsNotFoundError } from '../../profile-logic/errors';

import { createGeckoProfile } from '../fixtures/profiles/gecko-profile';
import JSZip from 'jszip';
import { serializeProfile } from '../../profile-logic/process-profile';
import {
serializeProfile,
processProfile,
} from '../../profile-logic/process-profile';
import {
getProfileFromTextSamples,
addMarkersToThreadWithCorrespondingSamples,
} from '../fixtures/profiles/processed-profile';
import { getHumanReadableTracks } from '../fixtures/profiles/tracks';
import { waitUntilState } from '../fixtures/utils';

import { compress } from '../../utils/gz';

Expand Down Expand Up @@ -1360,6 +1367,188 @@ describe('actions/receive-profile', function() {
expect(nodeData.selfTime).toBe(4);
});
});

describe('getProfilesFromRawUrl', function() {
function fetch200Response(profile: string) {
return {
ok: true,
status: 200,
headers: {
get: () => 'application/json',
},
json: () => Promise.resolve(JSON.parse(profile)),
};
}

async function setup(location: Object, requiredProfile: number = 1) {
const profile = _getSimpleProfile();
const geckoProfile = createGeckoProfile();

// Add mock fetch response for the required number of times.
// Usually it's 1 but it can be also 2 for `compare` dataSource.
for (let i = 0; i < requiredProfile; i++) {
window.fetch.mockResolvedValueOnce(
fetch200Response(serializeProfile(profile))
);
}

const geckoProfiler = {
getProfile: jest.fn().mockResolvedValue(geckoProfile),
getSymbolTable: jest
.fn()
.mockRejectedValue(new Error('No symbol tables available')),
};
window.geckoProfilerPromise = Promise.resolve(geckoProfiler);

simulateSymbolStoreHasNoCache();

// Silence the logs coming from the promise rejections above.
jest.spyOn(console, 'log').mockImplementation(() => {});

const store = blankStore();
await store.dispatch(getProfilesFromRawUrl(location));

// To find stupid mistakes more easily, check that we didn't get a fatal
// error here. If we got one, let's rethrow the error.
const view = getView(store.getState());
if (view.phase === 'FATAL_ERROR') {
throw view.error;
}

const waitUntilPhase = phase =>
waitUntilState(store, state => getView(state).phase === phase);

const waitUntilSymbolication = () =>
waitUntilState(
store,
state => ProfileViewSelectors.getSymbolicationStatus(state) === 'DONE'
);

return {
profile,
geckoProfile,
waitUntilPhase,
waitUntilSymbolication,
...store,
};
}

beforeEach(function() {
// The stub makes it easy to return different values for different
// arguments. Here we define the default return value because there is no
// argument specified.
window.fetch = jest.fn();
window.fetch.mockImplementation(() =>
Promise.reject(new Error('No more answers have been configured.'))
);
});

afterEach(function() {
delete window.fetch;
delete window.geckoProfilerPromise;
});

it('retrieves profile from a `public` data source and loads it', async function() {
const { profile, getState, dispatch } = await setup({
pathname: '/public/fakehash/',
search: '?thread=0&v=4',
hash: '',
});

// Check if we loaded the profile data successfully.
expect(ProfileViewSelectors.getProfile(getState())).toEqual(profile);
expect(getView(getState()).phase).toBe('PROFILE_LOADED');

// Check if we can successfully finalize the profile view.
await dispatch(finalizeProfileView());
expect(getView(getState()).phase).toBe('DATA_LOADED');
});

it('retrieves profile from a `from-url` data source and loads it', async function() {
const { profile, getState, dispatch } = await setup({
// '/from-url/https://fakeurl.com/fakeprofile.json/'
pathname: '/from-url/https%3A%2F%2Ffakeurl.com%2Ffakeprofile.json/',
search: '',
hash: '',
});

// Check if we loaded the profile data successfully.
expect(ProfileViewSelectors.getProfile(getState())).toEqual(profile);
expect(getView(getState()).phase).toBe('PROFILE_LOADED');

// Check if we can successfully finalize the profile view.
await dispatch(finalizeProfileView());
expect(getView(getState()).phase).toBe('DATA_LOADED');
});

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(
{
pathname: '/compare/FAKEURL/',
search: oneLineTrim`
?profiles[]=${encodeURIComponent(url1)}
&profiles[]=${encodeURIComponent(url2)}
`,
hash: '',
},
2
);

// Check if we loaded the profile data successfully.
expect(getView(getState()).phase).toBe('PROFILE_LOADED');

// Check if we can successfully finalize the profile view.
await dispatch(finalizeProfileView());
expect(getView(getState()).phase).toBe('DATA_LOADED');
});

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

// Differently, `from-addon` calls the finalizeProfileView internally,
// we don't need to call it again.
await waitUntilPhase('DATA_LOADED');
const processedProfile = processProfile(geckoProfile);
expect(ProfileViewSelectors.getProfile(getState())).toEqual(
processedProfile
);
});

it('finishes symbolication for `from-addon` data source', async function() {
const { waitUntilSymbolication } = await setup(
{
pathname: '/from-addon/',
search: '',
hash: '',
},
0
);

// It should successfully symbolicate the profiles that are loaded from addon.
await waitUntilSymbolication();
});

it('does not retrieve profile from other data sources', async function() {
['/none/', '/from-file/', 'local'].forEach(async sourcePath => {
await expect(
setup({
pathname: sourcePath,
search: '',
hash: '',
})
).rejects.toThrow();
});
});
});
});

/**
Expand Down

0 comments on commit 3067dda

Please sign in to comment.