react-native
Version:
A framework for building native apps using React
541 lines (477 loc) • 16.8 kB
JavaScript
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
;
jest.disableAutomock();
jest.mock('worker-farm', () => () => () => {})
.mock('timers', () => ({setImmediate: fn => setTimeout(fn, 0)}))
.mock('uglify-js')
.mock('crypto')
.mock(
'../symbolicate',
() => ({createWorker: jest.fn().mockReturnValue(jest.fn())}),
)
.mock('../../Bundler')
.mock('../../AssetServer')
.mock('../../lib/declareOpts')
.mock('../../node-haste')
.mock('../../Logger')
.mock('../../lib/GlobalTransformCache');
describe('processRequest', () => {
let Bundler, Server, AssetServer, Promise, symbolicate;
beforeEach(() => {
jest.resetModules();
Bundler = require('../../Bundler');
Server = require('../');
AssetServer = require('../../AssetServer');
Promise = require('promise');
symbolicate = require('../symbolicate');
});
let server;
const options = {
projectRoots: ['root'],
blacklistRE: null,
cacheVersion: null,
polyfillModuleNames: null,
reporter: require('../../lib/reporting').nullReporter,
};
const makeRequest = (reqHandler, requrl, reqOptions) => new Promise(resolve =>
reqHandler(
{url: requrl, headers:{}, ...reqOptions},
{
statusCode: 200,
headers: {},
getHeader(header) { return this.headers[header]; },
setHeader(header, value) { this.headers[header] = value; },
writeHead(statusCode) { this.statusCode = statusCode; },
end(body) {
this.body = body;
resolve(this);
},
},
{next: () => {}},
)
);
const invalidatorFunc = jest.fn();
let requestHandler;
beforeEach(() => {
Bundler.prototype.bundle = jest.fn(() =>
Promise.resolve({
getModules: () => [],
getSource: () => 'this is the source',
getSourceMap: () => ({version: 3}),
getSourceMapString: () => 'this is the source map',
getEtag: () => 'this is an etag',
}));
Bundler.prototype.invalidateFile = invalidatorFunc;
Bundler.prototype.getResolver =
jest.fn().mockReturnValue({
getDependencyGraph: jest.fn().mockReturnValue({
getHasteMap: jest.fn().mockReturnValue({on: jest.fn()}),
load: jest.fn(() => Promise.resolve()),
}),
});
server = new Server(options);
requestHandler = server.processRequest.bind(server);
});
it('returns JS bundle source on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
null
).then(response =>
expect(response.body).toEqual('this is the source')
);
});
it('returns JS bundle source on request of *.bundle (compat)', () => {
return makeRequest(
requestHandler,
'mybundle.runModule.bundle'
).then(response =>
expect(response.body).toEqual('this is the source')
);
});
it('returns ETag header on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(response => {
expect(response.getHeader('ETag')).toBeDefined();
});
});
it('returns 304 on request of *.bundle when if-none-match equals the ETag', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
{headers : {'if-none-match' : 'this is an etag'}}
).then(response => {
expect(response.statusCode).toEqual(304);
});
});
it('returns sourcemap on request of *.map', () => {
return makeRequest(
requestHandler,
'mybundle.map?runModule=true'
).then(response =>
expect(response.body).toEqual('this is the source map')
);
});
it('works with .ios.js extension', () => {
return makeRequest(
requestHandler,
'index.ios.includeRequire.bundle'
).then(response => {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'index.ios.js',
inlineSourceMap: false,
minify: false,
generateSourceMaps: false,
hot: false,
runModule: true,
sourceMapUrl: 'index.ios.includeRequire.map',
dev: true,
platform: undefined,
onProgress: jasmine.any(Function),
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
});
});
});
it('passes in the platform param', function() {
return makeRequest(
requestHandler,
'index.bundle?platform=ios'
).then(function(response) {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'index.js',
inlineSourceMap: false,
minify: false,
generateSourceMaps: false,
hot: false,
runModule: true,
sourceMapUrl: 'index.map?platform=ios',
dev: true,
platform: 'ios',
onProgress: jasmine.any(Function),
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
});
});
});
it('passes in the assetPlugin param', function() {
return makeRequest(
requestHandler,
'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2'
).then(function(response) {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'index.js',
inlineSourceMap: false,
minify: false,
generateSourceMaps: false,
hot: false,
runModule: true,
sourceMapUrl: 'index.map?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
dev: true,
platform: undefined,
onProgress: jasmine.any(Function),
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: ['assetPlugin1', 'assetPlugin2'],
});
});
});
describe('file changes', () => {
it('invalides files in bundle when file is updated', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(() => {
server.onFileChange('all', options.projectRoots[0] + '/path/file.js');
expect(invalidatorFunc.mock.calls[0][0]).toEqual('root/path/file.js');
});
});
it('does not rebuild the bundles that contain a file when that file is changed', () => {
const bundleFunc = jest.fn();
bundleFunc
.mockReturnValueOnce(
Promise.resolve({
getModules: () => [],
getSource: () => 'this is the first source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
)
.mockReturnValue(
Promise.resolve({
getModules: () => [],
getSource: () => 'this is the rebuilt source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
);
Bundler.prototype.bundle = bundleFunc;
server = new Server(options);
requestHandler = server.processRequest.bind(server);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
jest.runAllTicks();
server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
jest.runAllTimers();
jest.runAllTicks();
expect(bundleFunc.mock.calls.length).toBe(1);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response =>
expect(response.body).toEqual('this is the rebuilt source')
);
jest.runAllTicks();
});
it(
'does not rebuild the bundles that contain a file ' +
'when that file is changed, even when hot loading is enabled',
() => {
const bundleFunc = jest.fn();
bundleFunc
.mockReturnValueOnce(
Promise.resolve({
getModules: () => [],
getSource: () => 'this is the first source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
)
.mockReturnValue(
Promise.resolve({
getModules: () => [],
getSource: () => 'this is the rebuilt source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
);
Bundler.prototype.bundle = bundleFunc;
server = new Server(options);
server.setHMRFileChangeListener(() => {});
requestHandler = server.processRequest.bind(server);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
jest.runAllTicks();
server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
jest.runAllTimers();
jest.runAllTicks();
expect(bundleFunc.mock.calls.length).toBe(1);
server.setHMRFileChangeListener(null);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the rebuilt source');
expect(bundleFunc.mock.calls.length).toBe(2);
});
jest.runAllTicks();
});
});
describe('/onchange endpoint', () => {
let EventEmitter;
let req;
let res;
beforeEach(() => {
EventEmitter = require.requireActual('events').EventEmitter;
req = scaffoldReq(new EventEmitter());
req.url = '/onchange';
res = {
writeHead: jest.fn(),
end: jest.fn(),
};
});
it('should hold on to request and inform on change', () => {
server.processRequest(req, res);
server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
jest.runAllTimers();
expect(res.end).toBeCalledWith(JSON.stringify({changed: true}));
});
it('should not inform changes on disconnected clients', () => {
server.processRequest(req, res);
req.emit('close');
jest.runAllTimers();
server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
jest.runAllTimers();
expect(res.end).not.toBeCalled();
});
});
describe('/assets endpoint', () => {
it('should serve simple case', () => {
const req = scaffoldReq({url: '/assets/imgs/a.png'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
server.processRequest(req, res);
jest.runAllTimers();
expect(res.end).toBeCalledWith('i am image');
});
it('should parse the platform option', () => {
const req = scaffoldReq({url: '/assets/imgs/a.png?platform=ios'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
server.processRequest(req, res);
jest.runAllTimers();
expect(AssetServer.prototype.get).toBeCalledWith('imgs/a.png', 'ios');
expect(res.end).toBeCalledWith('i am image');
});
it('should serve range request', () => {
const req = scaffoldReq({
url: '/assets/imgs/a.png?platform=ios',
headers: {range: 'bytes=0-3'},
});
const res = {end: jest.fn(), writeHead: jest.fn(), setHeader: jest.fn()};
const mockData = 'i am image';
AssetServer.prototype.get.mockImplementation(() => Promise.resolve(mockData));
server.processRequest(req, res);
jest.runAllTimers();
expect(AssetServer.prototype.get).toBeCalledWith('imgs/a.png', 'ios');
expect(res.end).toBeCalledWith(mockData.slice(0, 4));
});
it('should serve assets files\'s name contain non-latin letter', () => {
const req = scaffoldReq({url: '/assets/imgs/%E4%B8%BB%E9%A1%B5/logo.png'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
server.processRequest(req, res);
jest.runAllTimers();
expect(AssetServer.prototype.get).toBeCalledWith(
'imgs/\u{4E3B}\u{9875}/logo.png',
undefined
);
expect(res.end).toBeCalledWith('i am image');
});
});
describe('buildbundle(options)', () => {
it('Calls the bundler with the correct args', () => {
return server.buildBundle({
entryFile: 'foo file',
}).then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'foo file',
inlineSourceMap: false,
minify: false,
hot: false,
runModule: true,
dev: true,
platform: undefined,
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
})
);
});
});
describe('buildBundleFromUrl(options)', () => {
it('Calls the bundler with the correct args', () => {
return server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&runModule=false')
.then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'path/to/foo.js',
inlineSourceMap: false,
minify: false,
generateSourceMaps: true,
hot: false,
runModule: false,
sourceMapUrl: '/path/to/foo.map?dev=false&runModule=false',
dev: false,
platform: undefined,
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
})
);
});
});
describe('/symbolicate endpoint', () => {
let symbolicationWorker;
beforeEach(() => {
symbolicationWorker = symbolicate.createWorker();
symbolicationWorker.mockReset();
});
it('should symbolicate given stack trace', () => {
const inputStack = [{
file: 'http://foo.bundle?platform=ios',
lineNumber: 2100,
column: 44,
customPropShouldBeLeftUnchanged: 'foo',
}];
const outputStack = [{
source: 'foo.js',
line: 21,
column: 4,
}];
const body = JSON.stringify({stack: inputStack});
expect.assertions(2);
symbolicationWorker.mockImplementation(stack => {
expect(stack).toEqual(inputStack);
return outputStack;
});
return makeRequest(
requestHandler,
'/symbolicate',
{rawBody: body},
).then(response =>
expect(JSON.parse(response.body)).toEqual({stack: outputStack}));
});
});
describe('/symbolicate handles errors', () => {
it('should symbolicate given stack trace', () => {
const body = 'clearly-not-json';
console.error = jest.fn();
return makeRequest(
requestHandler,
'/symbolicate',
{rawBody: body}
).then(response => {
expect(response.statusCode).toEqual(500);
expect(JSON.parse(response.body)).toEqual({
error: jasmine.any(String),
});
expect(console.error).toBeCalled();
});
});
});
describe('_getOptionsFromUrl', () => {
it('ignores protocol, host and port of the passed in URL', () => {
const short = '/path/to/entry-file.js??platform=ios&dev=true&minify=false';
const long = `http://localhost:8081${short}`;
expect(server._getOptionsFromUrl(long))
.toEqual(server._getOptionsFromUrl(short));
});
});
// ensures that vital properties exist on fake request objects
function scaffoldReq(req) {
if (!req.headers) {
req.headers = {};
}
return req;
}
});