fugafacere
Version:
A pure-JS implementation of the W3C's Canvas-2D Context API that can run on top of either Expo Graphics or a browser WebGL context.
464 lines (415 loc) • 13.6 kB
JavaScript
import React from 'react';
import { Dimensions, Linking, NativeModules, ScrollView, Text, View } from 'react-native';
import Expo2DContext from 'expo-2d-context';
import { GLView } from 'expo-gl';
import { registerRootComponent } from 'expo';
import Constants from 'expo-constants';
import jasmineModule from 'jasmine-core/lib/jasmine-core/jasmine';
import Immutable from 'immutable';
let { ExponentTest } = NativeModules;
// List of all modules for tests. Each file path must be statically present for
// the packager to pick them all up.
function getTestModules() {
let modules = [
require('./2d.canvas.js'),
require('./2d.clearRect.js'),
require('./2d.composite.js'),
require('./2d.coordinatespace.js'),
require('./2d.drawImage.js'),
require('./2d.fillRect.js'),
require('./2d.fillStyle.js'),
require('./2d.getcontext.js'),
require('./2d.gradient.js'),
require('./2d.imageData.js'),
require('./2d.line.js'),
require('./2d.missingargs.js'),
require('./2d.path.js'),
require('./2d.pattern.js'),
require('./2d.reset.js'),
require('./2d.scaled.js'),
require('./2d.shadow.js'),
require('./2d.state.js'),
require('./2d.strokeRect.js'),
require('./2d.strokeStyle.js'),
require('./2d.transformation.js'),
require('./2d.type.js'),
require('./2d.voidreturn.js')
];
return modules;
}
class App extends React.Component {
// --- Lifecycle -------------------------------------------------------------
constructor(props, context) {
super(props, context);
this.state = App.initialState;
this._results = '';
this._failures = '';
this._scrollViewRef = null;
}
componentDidMount() {
if (!useTestPad) {
Linking.getInitialURL().then(url => url && this._runTests(url));
}
}
// --- Test running ----------------------------------------------------------
static initialState = {
portalChildShouldBeVisible: false,
testRunnerError: null,
state: Immutable.fromJS({
suites: [],
path: ['suites'], // Path to current 'children' List in state
}),
};
setPortalChild = testPortal => this.setState({ testPortal });
cleanupPortal = () => new Promise(resolve => this.setState({ testPortal: null }, resolve));
async _runTests(uri) {
// Reset results state
this.setState(App.initialState);
const { jasmineEnv, jasmine } = await this._setupJasmine();
// Load tests, confining to the ones named in the uri
let modules = getTestModules();
if (uri && uri.indexOf('+') > -1) {
const deepLink = decodeURI(uri.substring(uri.indexOf('+') + 1));
const filterJSON = JSON.parse(deepLink);
if (filterJSON.includeModules) {
console.log('Only testing these modules: ' + JSON.stringify(filterJSON.includeModules));
const includeModulesRegexes = filterJSON.includeModules.map(m => new RegExp(m));
modules = modules.filter(m => {
for (let i = 0; i < includeModulesRegexes.length; i++) {
if (includeModulesRegexes[i].test(m.name)) {
return true;
}
}
return false;
});
if (modules.length === 0) {
this.setState({
testRunnerError: `No tests were found that satisfy ${deepLink}`,
});
return;
}
}
}
modules.forEach(m =>
m.test(jasmine, { setPortalChild: this.setPortalChild, cleanupPortal: this.cleanupPortal })
);
jasmineEnv.execute();
}
async _setupJasmine() {
// Init
jasmineModule.DEFAULT_TIMEOUT_INTERVAL = 10000;
const jasmineCore = jasmineModule.core(jasmineModule);
const jasmineEnv = jasmineCore.getEnv();
// Add our custom reporters too
jasmineEnv.addReporter(this._jasmineSetStateReporter());
jasmineEnv.addReporter(this._jasmineConsoleReporter());
// Get the interface and make it support `async ` by default
const jasmine = jasmineModule.interface(jasmineCore, jasmineEnv);
const doneIfy = fn => async done => {
try {
await Promise.resolve(fn());
done();
} catch (e) {
done.fail(e);
}
};
const oldIt = jasmine.it;
jasmine.it = (desc, fn, t) => oldIt.apply(jasmine, [desc, doneIfy(fn), t]);
const oldXit = jasmine.xit;
jasmine.xit = (desc, fn, t) => oldXit.apply(jasmine, [desc, doneIfy(fn), t]);
const oldFit = jasmine.fit;
jasmine.fit = (desc, fn, t) => oldFit.apply(jasmine, [desc, doneIfy(fn), t]);
return {
jasmineCore,
jasmineEnv,
jasmine,
};
}
// A jasmine reporter that writes results to the console
_jasmineConsoleReporter(jasmineEnv) {
const failedSpecs = [];
return {
specDone(result) {
if (result.status === 'passed' || result.status === 'failed') {
// Open log group if failed
const grouping = result.status === 'passed' ? '---' : '+++';
const emoji = result.status === 'passed' ? ':green_heart:' : ':broken_heart:';
console.log(`${grouping} ${emoji} ${result.fullName}`);
this._results += `${grouping} ${result.fullName}\n`;
if (result.status === 'failed') {
this._failures += `${grouping} ${result.fullName}\n`;
result.failedExpectations.forEach(({ matcherName, message }) => {
console.log(`${matcherName}: ${message}`);
this._results += `${matcherName}: ${message}\n`;
this._failures += `${matcherName}: ${message}\n`;
});
failedSpecs.push(result);
}
}
},
suiteDone(result) {},
jasmineStarted() {
console.log('--- tests started');
},
jasmineDone() {
console.log('--- tests done');
console.log('--- send results to runner');
let result = JSON.stringify({
magic: '[TEST-SUITE-END]', // NOTE: Runner/Run.js waits to see this
failed: failedSpecs.length,
results: this._results,
});
console.log(result);
if (ExponentTest) {
// Native logs are truncated so log just the failures for now
ExponentTest.completed(
JSON.stringify({
failed: failedSpecs.length,
failures: this._failures,
})
);
}
},
};
}
// A jasmine reporter that writes results to this.state
_jasmineSetStateReporter(jasmineEnv) {
const app = this;
return {
suiteStarted(jasmineResult) {
app.setState(({ state }) => ({
state: state
.updateIn(state.get('path'), children =>
children.push(
Immutable.fromJS({
result: jasmineResult,
children: [],
specs: [],
})
)
)
.update('path', path => path.push(state.getIn(path).size, 'children')),
}));
},
suiteDone(jasmineResult) {
app.setState(({ state }) => ({
state: state
.updateIn(
state
.get('path')
.pop()
.pop(),
children =>
children.update(children.size - 1, child =>
child.set('result', child.get('result'))
)
)
.update('path', path => path.pop().pop()),
}));
},
specStarted(jasmineResult) {
app.setState(({ state }) => ({
state: state.updateIn(
state
.get('path')
.pop()
.pop(),
children =>
children.update(children.size - 1, child =>
child.update('specs', specs => specs.push(Immutable.fromJS(jasmineResult)))
)
),
}));
},
specDone(jasmineResult) {
if (app.state.testPortal) {
console.warn(
`The test portal has not been cleaned up by \`${jasmineResult.fullName}\`. Call \`cleanupPortal\` before finishing the test.`
);
}
app.setState(({ state }) => ({
state: state.updateIn(
state
.get('path')
.pop()
.pop(),
children =>
children.update(children.size - 1, child =>
child.update('specs', specs =>
specs.set(specs.size - 1, Immutable.fromJS(jasmineResult))
)
)
),
}));
},
};
}
// --- Rendering -------------------------------------------------------------
_renderSpecResult = r => {
const status = r.get('status') || 'running';
return (
<View
key={r.get('id')}
style={{
paddingLeft: 10,
marginVertical: 3,
borderColor: {
running: '#ff0',
passed: '#0f0',
failed: '#f00',
disabled: '#888',
}[status],
borderLeftWidth: 3,
}}>
<Text style={{ fontSize: 16 }}>
{
{
running: '😮 ',
passed: '😄 ',
failed: '😞 ',
}[status]
}
{r.get('description')} ({status})
</Text>
{r.get('failedExpectations').map((e, i) => <Text key={i}>{e.get('message')}</Text>)}
</View>
);
};
_renderSuiteResult = (r, depth) => {
const titleStyle =
depth == 0
? { marginBottom: 8, fontSize: 16, fontWeight: 'bold' }
: { marginVertical: 8, fontSize: 16 };
const containerStyle =
depth == 0
? {
paddingLeft: 16,
paddingVertical: 16,
borderBottomWidth: 1,
borderColor: '#ddd',
}
: { paddingLeft: 16 };
return (
<View key={r.get('result').get('id')} style={containerStyle}>
<Text style={titleStyle}>{r.get('result').get('description')}</Text>
{r.get('specs').map(this._renderSpecResult)}
{r.get('children').map(r => this._renderSuiteResult(r, depth + 1))}
</View>
);
};
_onScrollViewContentSizeChange = (contentWidth, contentHeight) => {
if (this._scrollViewRef) {
this._scrollViewRef.scrollTo({
y:
Math.max(0, contentHeight - Dimensions.get('window').height) +
Constants.statusBarHeight,
});
}
};
_renderPortal = () => {
const styles = {
top: 0,
left: 0,
right: 0,
bottom: 0,
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgb(255, 255, 255)',
opacity: this.state.portalChildShouldBeVisible ? 0.5 : 0,
};
if (this.state.testPortal) {
return (
<View style={styles} pointerEvents="none">
{this.state.testPortal}
</View>
);
}
};
render() {
if (useTestPad) {
let activeContexts = 0;
let glViews = [];
let glContexts = [];
let _onGLContextCreate = (gl) => {
glContexts.push(gl);
activeContexts += 1;
if (activeContexts == nContexts) {
testPad(glContexts)
}
};
for(let i=0; i < nContexts; i++) {
glViews.push(
<GLView key={i}
style={{ height: 300, width: 300 }}
onContextCreate={_onGLContextCreate}/>
)
}
return <View style={{flex: 1, flexDirection: 'column'}}> { glViews } </View>;
}
if (this.state.testRunnerError) {
return (
<View
style={{
flex: 1,
marginTop: Constants.statusBarHeight || 18,
alignItems: 'center',
justifyContent: 'center',
}}>
<Text style={{ color: 'red' }}>{this.state.testRunnerError}</Text>
</View>
);
}
return (
<View
style={{
flex: 1,
marginTop: Constants.statusBarHeight || 18,
alignItems: 'stretch',
justifyContent: 'center',
}}
testID="test_suite_container">
<ScrollView
style={{
flex: 1,
}}
contentContainerStyle={{
padding: 5,
}}
ref={ref => (this._scrollViewRef = ref)}
onContentSizeChange={this._onScrollViewContentSizeChange}>
{this.state.state.get('suites').map(r => this._renderSuiteResult(r, 0))}
</ScrollView>
{this._renderPortal()}
</View>
);
}
}
/////////////////////////////////////////////////////////////////////
// Nice quick way to get an expo canvas up when debugging things,
// just set this var to true and put your test code in testPad():
var useTestPad = false;
var nContexts = 2;
function _getPixel(ctx, x,y)
{
var imgdata = ctx.getImageData(x, y, 1, 1);
return [ imgdata.data[0], imgdata.data[1], imgdata.data[2], imgdata.data[3] ];
}
async function testPad(glContexts) {
var ctx = new Expo2DContext(glContexts[0], {renderWithOffscreenBuffer: true});
var ctx2 = new Expo2DContext(glContexts[1], {renderWithOffscreenBuffer: true});
ctx2.fillStyle = '#0f0';
ctx2.fillRect(0, 0, 100, 50);
ctx2.flush()
let assetWidth = ctx2.gl.drawingBufferWidth;
let assetHeight = ctx2.gl.drawingBufferHeight;
let assetImage = ctx2.getImageData(0, 0, 10, 10);
ctx.fillStyle = '#f00';
ctx.drawImage(ctx2, 0, 0);
// ctx.fillRect(30,30,15,15);
ctx.flush()
}
//
/////////////////////////////////////////////////////////////////////
registerRootComponent(App);
;