automated tests support

This commit is contained in:
Val Erastov 2019-02-08 17:41:28 -08:00
parent 050e2c2348
commit 491e3695d5
28 changed files with 462 additions and 60 deletions

View file

@ -2,3 +2,8 @@ export default function capitalize(str) {
if (!str) return;
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function decapitalize(str) {
if (!str) return;
return str.charAt(0).toLowerCase() + str.slice(1);
}

View file

@ -23,6 +23,7 @@ export function readBrep(data) {
format: 'verbose',
data: normalizeTesselationData(faceData.tess, inverted, faceData.surface.normal)
};
bb._face.data.productionInfo = faceData.productionInfo;
if (faceData.ref !== undefined) {
bb._face.data.externals = {
ref: faceData.ref

View file

@ -1,5 +1,6 @@
import {enableAnonymousActionHint} from './anonHint';
import * as stream from 'lstream';
import {DEBUG_FLAGS} from '../debugFlags';
export function activate(context) {
@ -23,6 +24,9 @@ export function activate(context) {
return;
}
if (state.enabled) {
if (DEBUG_FLAGS.ACTION_RUN) {
console.log("RUNNING ACTION: " + id);
}
runner(context, data);
} else {
showAnonymousActionHint(id);

View file

@ -72,6 +72,7 @@ export function readSketchContour(contour, face) {
ab = ab.map(v => tr.apply(v).data());
path.push({TYPE: CURVE_TYPES.SEGMENT, a: ab[0], b: ab[1]});
}
path[path.length - 1].id = s.id;
});
return path;
}

View file

@ -0,0 +1,5 @@
export const DEBUG_FLAGS = {
ACTION_RUN: false
};

View file

@ -10,6 +10,7 @@ import {toLoops} from '../brep/io/brepLoopsFormat';
import {contributeComponent} from './dom/components/ContributedComponents';
import BrepDebuggerWindow, {BREP_DEBUG_WINDOW_VISIBLE} from '../brep/debug/debugger/BrepDebuggerWindow';
import curveTess from '../brep/geom/impl/curve/curve-tess';
import {DEBUG_FLAGS} from './debugFlags';
export function activate({bus, services, streams}) {
@ -17,7 +18,9 @@ export function activate({bus, services, streams}) {
addDebugSelectors(services);
services.action.registerActions(DebugActions);
services.menu.registerMenus([DebugMenuConfig]);
services.debug = {
FLAGS: DEBUG_FLAGS
};
streams.ui.controlBars.left.update(actions => [...actions, 'menu.debug']);
bus.enableState(BREP_DEBUG_WINDOW_VISIBLE, false);

View file

@ -71,6 +71,7 @@ export default function startApplication(callback) {
];
let allPlugins = [...preUIPlugins, ...plugins];
context.services.plugin = createPluginService(allPlugins, context);
defineStreams(allPlugins, context);
@ -98,3 +99,16 @@ export function activatePlugins(plugins, context) {
}
}
function createPluginService(plugins, context) {
function disposePlugins() {
for (let plugin of plugins) {
if (plugin.dispose) {
plugin.dispose(context);
}
}
}
return {
disposePlugins
};
}

View file

@ -72,6 +72,10 @@ export class MFace extends MObject {
}
}
get defaultSketchId() {
return this.id;
}
setSketch(sketch) {
if (!this.isPlaneBased) {

View file

@ -9,9 +9,9 @@ export function activate(context) {
const {streams, services} = context;
const [id, params] = parseHintsFromLocation();
const [id, hints] = parseHintsFromLocation();
processParams(params, context);
processParams(hints, context);
const sketchNamespace = id + '.sketch.';
const sketchStorageNamespace = STORAGE_PREFIX + sketchNamespace;
@ -40,20 +40,33 @@ export function activate(context) {
let dataStr = services.storage.get(services.project.projectStorageKey());
if (dataStr) {
let data = JSON.parse(dataStr);
if (data.history) {
services.craft.reset(data.history);
}
if (data.expressions) {
services.expressions.load(data.expressions);
}
loadData(data);
}
} catch (e) {
console.error(e);
}
}
function loadData(data) {
if (data.history) {
services.craft.reset(data.history);
}
if (data.expressions) {
services.expressions.load(data.expressions);
}
}
function empty() {
loadData({
history: [],
expressions: ""
});
}
services.project = {
id, sketchStorageKey, projectStorageKey, sketchStorageNamespace, getSketchURL, save, load
id, sketchStorageKey, projectStorageKey, sketchStorageNamespace, getSketchURL, save, load, empty,
hints
}
}

View file

@ -24,3 +24,7 @@ export function activate({streams, services}) {
// services.viewer.setCameraMode(CAMERA_MODE.ORTHOGRAPHIC);
}
export function dispose(ctx) {
ctx.services.viewer.dispose();
}

View file

@ -58,6 +58,10 @@ export default class Viewer {
this.setCameraMode(CAMERA_MODE.PERSPECTIVE);
}
}
dispose() {
this.sceneSetup.renderer.dispose();
}
}
export const CAMERA_MODE = {

View file

@ -0,0 +1,3 @@
export default function globalSketchId(sketchId, id) {
return sketchId + "/" + id;
}

View file

@ -6,6 +6,7 @@ import * as math from '../../math/math'
import {HashTable} from '../../utils/hashmap'
import {Constraints} from '../../sketcher/parametric';
import Joints from '../../../../modules/gems/joints';
import sketchObjectGlobalId from './sketchObjectGlobalId';
class SketchGeom {
@ -43,9 +44,7 @@ class SketchGeom {
}
export function ReadSketch(sketch, sketchId, readConstructionSegments) {
function getID(obj) {
return sketchId + "/" + obj.id;
}
const getID = obj => sketchObjectGlobalId(sketchId, obj.id);
const out = new SketchGeom();
let coiJoints = new Joints();

View file

@ -23,8 +23,10 @@ export function activate(ctx) {
if (evt.key.indexOf(prefix) < 0) return;
let sketchFaceId = evt.key.substring(prefix.length);
let sketchFace = services.cadRegistry.findFace(sketchFaceId);
updateSketchForFace(sketchFace);
services.viewer.requestRender();
if (sketchFace) {
updateSketchForFace(sketchFace);
services.viewer.requestRender();
}
};
services.storage.addListener(onSketchUpdate);
@ -70,7 +72,7 @@ export function activate(ctx) {
}
function updateSketchForFace(mFace) {
let sketch = readSketch(mFace.id);
let sketch = readSketch(mFace.defaultSketchId);
mFace.setSketch(sketch);
streams.sketcher.update.next(mFace);
}

View file

@ -38,5 +38,6 @@ export default {
},
math: {
vec
}
},
THREE: THREE
}

View file

@ -85,6 +85,10 @@ Viewer.prototype.dispose = function() {
this.toolManager.dispose();
};
Viewer.prototype.isDisposed = function() {
return this.canvas === null;
};
Viewer.prototype.setTransformation = function(a, b, c, d, e, f, zoom) {
this.transformation = [a, b, c, d, e, f];
this.scale = zoom;
@ -190,9 +194,11 @@ Viewer.prototype._createServiceLayers = function() {
};
Viewer.prototype.refresh = function() {
var viewer = this;
window.requestAnimationFrame( function() {
viewer.repaint();
const viewer = this;
window.requestAnimationFrame(function() {
if (!viewer.isDisposed()) {
viewer.repaint();
}
});
};

View file

@ -1,16 +0,0 @@
import * as test from '../test';
export default {
testPlanes: env => {
test.modeller(env.testTPI(tpi => {
tpi.services.action.run('PLANE');
console.dir(tpi);
env.done();
}));
},
};

View file

@ -0,0 +1,40 @@
import {assertEmpty, assertEquals, assertFaceIsPlane, assertTrue} from '../utils/asserts';
import sketchObjectGlobalId from '../../app/cad/sketch/sketchObjectGlobalId';
export const TEST_MODE = 'modellerUI';
export function testExtrudeFromSketch2(env, ui) {
// globalSketchId(sketchId, seg1.id)
}
export function testExtrudeFromSketch(env, ui) {
ui.openWizard('PLANE');
ui.wizardOK();
ui.selectFaces([0, 0, -10], [0, 0, 10]);
let sketchedFace = ui.context.services.selection.face.single;
let sketcherUI = ui.openSketcher();
let seg1 = sketcherUI.addSegment(-100, -100, 100, -100);
let seg2 = sketcherUI.addSegment(100, -100, 100, 100);
let seg3 = sketcherUI.addSegment(100, 100, -100, 100);
let seg4 = sketcherUI.addSegment(-100, 100, -100, -100);
ui.commitSketch();
ui.selectFaces([0, 0, -10], [0, 0, 10]);
ui.openWizard('EXTRUDE');
ui.wizardContext.updateParam('value', 200);
ui.wizardOK();
let [leftFace] = ui.rayCastFaces([-200, 0, 100], [200, 0, 100]);
let sketchId = sketchedFace.defaultSketchId;
assertTrue(leftFace.brepFace.data.productionInfo.originatedFromPrimitive,
sketchObjectGlobalId(sketchId, seg3.id));
assertTrue(leftFace.brepFace.data.productionInfo.role, "sweep");
env.done();
}

View file

@ -0,0 +1,81 @@
import {assertEmpty, assertEquals, assertFaceIsPlane, assertTrue} from '../utils/asserts';
export const TEST_MODE = 'modellerUI';
export function testCreatePlaneAtOriginDefaultXY(env, ui) {
ui.openWizard('PLANE');
ui.wizardOK();
assertFaceIsPlane(ui.rayCastFaces([0, 0, -10], [0, 0, 10])[0]);
env.done();
}
export function testCreatePlaneAtOriginXZ(env, ui) {
ui.openWizard('PLANE');
ui.wizardContext.updateParam('orientation', 'XZ');
ui.wizardOK();
assertFaceIsPlane(ui.rayCastFaces([0, -10, 0], [0, 10, 0])[0]);
env.done();
}
export function testCreatePlaneAtZY(env, ui) {
ui.openWizard('PLANE');
ui.wizardContext.updateParam('orientation', 'ZY');
ui.wizardOK();
assertFaceIsPlane(ui.rayCastFaces([-10, 0, 0], [10, 0, 0])[0]);
env.done();
}
export function testCreatePlaneAtOriginXYOffset(env, ui) {
ui.openWizard('PLANE');
ui.wizardContext.updateParam('depth', 100);
ui.wizardOK();
assertEmpty(ui.rayCastFaces([0, 0, -10], [0, 0, 10]));
assertFaceIsPlane(ui.rayCastFaces([0, 0, 90], [0, 0, 110])[0]);
env.done();
}
export function testCreatePlaneAtOriginXZOffset(env, ui) {
ui.openWizard('PLANE');
ui.wizardContext.updateParam('orientation', 'XZ');
ui.wizardContext.updateParam('depth', 100);
ui.wizardOK();
assertEmpty(ui.rayCastFaces([0, -10, 0], [0, 10, 0]));
assertFaceIsPlane(ui.rayCastFaces([0, 90, 0], [0, 110, 0])[0]);
env.done();
}
export function testCreatePlaneAtOriginZYOffset(env, ui) {
ui.openWizard('PLANE');
ui.wizardContext.updateParam('orientation', 'ZY');
ui.wizardContext.updateParam('depth', 100);
ui.wizardOK();
assertEmpty(ui.rayCastFaces([-10, 0, 0], [10, 0, 0]));
assertFaceIsPlane(ui.rayCastFaces([90, 0, 0], [110, 0, 0])[0]);
env.done();
}
export function testCreatePlaneParallelToOther(env, ui) {
ui.openWizard('PLANE');
ui.wizardContext.updateParam('orientation', 'ZY');
ui.wizardContext.updateParam('depth', 100);
ui.wizardOK();
assertEmpty(ui.rayCastFaces([-10, 0, 0], [10, 0, 0]));
let captured = ui.rayCastFaces([90, 0, 0], [210, 0, 0]);
assertTrue(captured.length === 1);
let baseFace = captured[0];
ui.openWizard('PLANE');
ui.wizardContext.updateParam('parallelTo', baseFace.id);
ui.wizardContext.updateParam('depth', 100);
ui.wizardOK();
captured = ui.rayCastFaces([90, 0, 0], [210, 0, 0]);
assertTrue(captured.length === 2);
assertTrue(captured[0].id === baseFace.id);
assertTrue(captured[1].id !== baseFace.id);
env.done();
}

View file

@ -11,7 +11,7 @@ export class Menu {
.on('click', (e) => this.popup.hide())
.on('contextmenu', (e) => {
const target = $(e.target).closest('.right-click-menu');
if (target.length == 0) return true;
if (target.length === 0) return true;
return this.onShowMenu(e, target);
});
}

10
web/test/modes.js Normal file
View file

@ -0,0 +1,10 @@
import * as test from './test';
import modellerUISubject from './utils/subjects/modeller/modellerUISubject';
export const modellerUI = func => env => {
test.emptyModeller(env.test(win => {
let subject = modellerUISubject(win.__CAD_APP);
func(env, subject);
}));
};

View file

@ -4,17 +4,19 @@ body {
}
#sandbox {
position: fixed;
border: 3px plum solid;
box-sizing: border-box;
width: 100%;
height: 60%;
right: 0;
bottom: 0;
height: 40%;
max-height: 500px;
width: 40%;
max-width: 500px;
background-color: #233930;
}
#test-list {
width: 100%;
height: ~"calc(40% - 22px)";
overflow-y: scroll;
}
#sandbox iframe {

View file

@ -1,3 +1,6 @@
import * as test from './test';
import * as modes from './modes';
export default {
SketcherObjects: [
TestCase('segment'),
@ -20,7 +23,8 @@ export default {
],
Craft: [
TestCase('craft'),
TestCase('craftPlane'),
TestCase('craftExtrude'),
],
BREP: [
@ -36,11 +40,22 @@ export default {
};
function TestCase(name) {
let tests = require('./cases/' + name).default;
tests = Object.keys(tests).filter(key => key.startsWith('test')).map(key => ({
name: key,
func: tests[key]
}));
let testModule = require('./cases/' + name);
let tests;
function registerTests(testsHolder, helperWrapper) {
tests = testsHolder;
tests = Object.keys(tests).filter(key => key.startsWith('test')).map(key => ({
name: key,
func: helperWrapper(tests[key])
}));
}
let mode = modes[testModule.TEST_MODE];
if (mode) {
registerTests(testModule, mode);
} else {
registerTests(testModule.default, func => env => func(env));
}
return {
name, tests
}

View file

@ -52,18 +52,18 @@ export class TestEnv {
}
}
testTPI(testBlock) {
return this.test(function(win, app) {
testBlock(app.TPI);
});
}
assertTrue(stmt, msg) {
if (!stmt) {
this.fail('assertTrue fails.', msg);
}
}
assertEmpty(array, msg) {
if (array.length !== 0) {
this.fail('assertEmpty fails. Array length = ' + array.length, msg);
}
}
assertFalse(stmt, msg) {
if (stmt) {
this.fail('assertFalse fails.', msg);
@ -95,7 +95,7 @@ export class TestEnv {
assertData(expected, actual) {
const expectedJSON = JSON.stringify(expected).replace(/\s/g, '');
const actualJSON = JSON.stringify(actual).replace(/\s/g, '');
if (actualJSON != expectedJSON) {
if (actualJSON !== expectedJSON) {
console.log('EXPECTED:');
console.log(this.prettyJSON(expected));
console.log('ACTUAL:');
@ -141,7 +141,7 @@ function checkSimilarity(data1, data2) {
const info1 = info(data1);
const info2 = info(data2);
console.log(info1 + " : " + info2);
return info1 == info2;
return info1 === info2;
}
@ -174,8 +174,22 @@ export function sketch(callback) {
load('/sketcher.html#' + TEST_PROJECT, callback, SKETCHER_API);
}
let ModellerWin = null;
export function modeller(callback) {
load('/index.html#' + TEST_PROJECT, callback, MODELLER_API);
if (ModellerWin != null) {
ModellerWin.__CAD_APP.services.project.empty();
callback(ModellerWin, MODELLER_API(ModellerWin));
return;
}
load('/index.html#' + TEST_PROJECT, (win, api) => {
ModellerWin = win;
let ctx = win.__CAD_APP;
ctx.streams.lifecycle.projectLoaded.attach(loaded => {
if (loaded) {
callback(win, api);
}
})
}, MODELLER_API);
}
export function emptyModeller(callback) {

49
web/test/utils/asserts.js Normal file
View file

@ -0,0 +1,49 @@
import {FailError} from '../test';
export function fail(msg, optionalMsg) {
throw new FailError(msg + (optionalMsg === undefined ? '' : ' ' + optionalMsg));
}
export function assertTrue(stmt, msg) {
if (!stmt) {
fail('assertTrue fails.', msg);
}
}
export function assertEmpty(array, msg) {
if (array.length !== 0) {
fail('assertEmpty fails. Array length = ' + array.length, msg);
}
}
export function assertFalse(stmt, msg) {
if (stmt) {
fail('assertFalse fails.', msg);
}
}
export function assertEquals(expected, actual, msg) {
if (expected !== actual) {
fail('assertEquals: Expected: ' + expected + ' but was ' + actual, msg);
}
}
export function assertFloatEquals(expected, actual, msg) {
if (Math.abs(expected - actual) >= 1E-6) {
fail('assertFloatEquals: Expected: ' + expected + ' but was ' + actual, msg);
}
}
export function assertPointXY2DEquals(expectedX, expectedY, actual, msg) {
if (actual.x !== expectedX || actual.y !== expectedY) {
fail('assertPoint2DEquals: Expected: (' + expectedX + ', ' + expectedY + ') but was (' + actual.x + ', ' + actual.y + ')', msg);
}
}
export function assertPoint2DEquals(expected, actial, msg) {
this.assertPointXY2DEquals(expected.x, expected.y, actial, msg);
}
export function assertFaceIsPlane(face) {
assertTrue(face.shell.surfacePrototype !== undefined);
}

View file

@ -46,6 +46,14 @@ export function addSegment(app, aX, aY, bX, bY) {
return segment;
}
export function addSegmentInModel(app, aX, aY, bX, bY) {
[aX, aY] = modelToScreen(app.viewer, aX, aY);
[bX, bY] = modelToScreen(app.viewer, bX, bY);
return addSegment(app, aX, aY, bX, bY);
}
export function polyLine(app) {
app.actions['addMultiSegment'].action();
const tool = app.viewer.toolManager.tool;
@ -79,4 +87,13 @@ export class TestSegment {
add(app) {
return addSegment(app, this.a.x, this.a.y, this.b.x, this.b.y);
}
}
}
function modelToScreen(viewer, x, y) {
let modelToScreenMx = viewer.screenToModelMatrix.invert();
[x, y] = modelToScreenMx.apply3([x, y, 0]);
x /= viewer.retinaPxielRatio;
y = (viewer.canvas.height - y) / viewer.retinaPxielRatio;
return [x, y];
}

View file

@ -0,0 +1,95 @@
import {TestMouseEvent} from '../../mouse-event';
import {getAttribute} from '../../../../../modules/scene/objectData';
import {FACE} from '../../../../app/cad/scene/entites';
import {createSubjectFromInPlaceSketcher} from './sketcherUISubject';
export default ctx => {
function openWizard(operationId) {
ctx.services.action.run(operationId);
}
function wizardOK() {
ctx.services.wizard.applyWorkingRequest();
}
function sceneMouseEvent(type, x, y) {
let domEl = ctx.services.viewer.sceneSetup.domElement();
let xMid = Math.round(domEl.offsetWidth / 2 + x);
let yMid = Math.round(domEl.offsetHeight / 2 + y);
domEl.dispatchEvent(mouseEvent(type, xMid, yMid));
}
function clickOnScene(x, y) {
sceneMouseEvent('mousedown', x, y);
sceneMouseEvent('mouseup', x, y);
}
function rayCast(from3, to3) {
const THREE = ctx.services.tpi.THREE;
let raycaster = new THREE.Raycaster();
let from = new THREE.Vector3().fromArray(from3);
let to = new THREE.Vector3().fromArray(to3);
let dir = to.sub(from);
let dist = dir.length();
raycaster.set(from, dir.normalize());
return raycaster.intersectObjects( ctx.services.cadScene.workGroup.children, true ).filter(h => h.distance <= dist);
}
function rayCastFaces(from, to) {
let models = rayCast(from, to).map(h => {
if (h.face) {
let faceV = getAttribute(h.face, FACE);
if (faceV && faceV.model) {
return faceV.model;
}
}
});
let out = [];
models.forEach(m => {
if (!!m && !out.includes(m)) {
out.push(m);
}
});
return out;
}
function selectFaces(from, to) {
rayCastFaces(from, to).forEach(face => ctx.services.pickControl.pick(face));
}
function getWizardContext() {
return ctx.streams.wizard.wizardContext.value
}
function openSketcher() {
ctx.services.action.run('EditFace');
return createSubjectFromInPlaceSketcher(ctx);
}
function commitSketch() {
ctx.services.action.run('sketchSaveAndExit');
}
return {
context: ctx,
openWizard, wizardOK, sceneMouseEvent, clickOnScene,
rayCastFaces, selectFaces, openSketcher, commitSketch,
get wizardContext() { return getWizardContext()}
};
}
function mouseEvent(type, x, y) {
return new MouseEvent(type, {
bubbles: true,
screenX: x,
screenY: y,
clientX: x,
clientY: y,
pageX: x,
pageY: y,
offsetX: x,
offsetY: y,
});
}

View file

@ -0,0 +1,26 @@
import * as sketcher_utils from '../../../utils/sketcher-utils'
import {decapitalize} from '../../../../../modules/gems/capitalize';
export function createSubjectFromInPlaceSketcher(ctx) {
let actions = {};
for (const actionId of Object.keys(ctx.streams.action.state)) {
if (actionId.startsWith('sketch')) {
let oldId = decapitalize(actionId.substring(6));
actions[oldId] = {
action: () => ctx.services.action.run(actionId)
}
}
}
const oldStyleSketcherApp = {
viewer: ctx.services.sketcher.inPlaceEditor.viewer,
actions
};
return {
addSegment: sketcher_utils.addSegmentInModel.bind(this, oldStyleSketcherApp)
}
}