// NetTango Copyright (C) Michael S. Horn, Uri Wilensky, and Corey Brady. https://github.com/NetLogo/NetTango
import { AttributeTypes } from "../src/attribute-types"
import { BlockPlacement } from "../src/blocks/block-placement"
import { defaultOptions, NetTango } from "../src/nettango"
import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from "../src/nettango-defaults"
import { ObjectUtils } from "../src/utils/object-utils"
import { VersionManager } from "../src/versions/version-manager"
beforeEach( () => document.body.innerHTML = `
` )
const NETLOGO_MODEL_1 = {
"version": 1,
"blocks": [
{
"id": 23,
"action": "wolf actions",
"type": "nlogo:procedure",
"limit": 1,
"format": "to wolf-actions",
"blockColor": "#bb5555",
"required": true
},
{
"id": 24,
"action": "forward",
"format": "forward ({0} + {P0})",
"type": "nlogo:command",
"params": [
{
"id": 3,
"type": "range",
"name": "steps",
"unit": "",
"value": 1,
"default": 1,
"random": false,
"step": 0.1,
"min": 0,
"max": 3
}
],
"properties": [
{
"id": 4,
"type": "int",
"step": 2,
"name": "",
"unit": "",
"value": 2,
}
]
},
{
"id": 25,
"action": "sheep actions",
"type": "nlogo:procedure",
"limit": 1,
"format": "to sheep-actions",
"blockColor": "#bb5555",
"required": true
}
],
"program": {
"chains": [
[
{
"id": 23,
"instanceId": 3,
"action": "wolf actions",
"type": "nlogo:procedure",
"format": "to wolf-actions",
"required": true,
"x": 4,
"y": 8
},
{
"id": 24,
"instanceId": 4,
"action": "forward",
"type": "nlogo:command",
"format": "forward ({0} + {P0})",
"required": false,
"x": 4,
"y": 11.4,
"params": [
{
"id": 3,
"type": "range",
"name": "steps",
"unit": "",
"value": 1,
"default": 1,
"random": false,
"step": 0.1,
"min": 0,
"max": 3
}
],
"properties": [
{
"id": 4,
"type": "int",
"step": 2,
"name": "",
"unit": "",
"value": 2
}
]
}
],
[
{
"id": 25,
"instanceId": 5,
"action": "sheep actions",
"type": "nlogo:procedure",
"format": "to sheep-actions",
"required": true,
"x": 7.5607421875,
"y": 20
},
{
"id": 24,
"instanceId": 6,
"action": "forward",
"type": "nlogo:command",
"format": "forward ({0} + {P0})",
"required": false,
"x": 7.5607421875,
"y": 23.4,
"params": [
{
"id": 3,
"type": "range",
"name": "steps",
"unit": "",
"value": 2,
"default": 1,
"random": false,
"step": 0.1,
"min": 0,
"max": 3
}
],
"properties": [
{
"id": 4,
"type": "int",
"name": "",
"unit": "",
"value": 3,
"step": 2
}
]
}
]
]
}
}
function copyJson(json: any) { return ObjectUtils.clone(json) }
function formatAttributeForNetTangoExtension(containerId: string, blockId: number, instanceId: number, attributeId: number, value: any, attributeType: AttributeTypes, isProperty: boolean) {
const prefix = isProperty ? "P" : ""
return `__${containerId}_${blockId}_${instanceId}_${prefix}${attributeId}`
}
function formatAttributeAsValue(containerId: string, blockId: number, instanceId: number, attributeId: number, value: any, attributeType: AttributeTypes): string {
return value.toString()
}
test("Smoke test of NetTango restore and save", () => {
NetTango.restore("NetLogo", "nt-canvas", {}, formatAttributeForNetTangoExtension, defaultOptions)
const result = NetTango.save("nt-canvas")
const expected = VersionManager.updateWorkspace({
"version": 5,
"height": DEFAULT_HEIGHT, "width": DEFAULT_WIDTH,
"blocks": [],
"program": { "chains": [] }
})
expect(result).toStrictEqual(expected)
})
test("Remove a parameter from a block that is in a chain", () => {
const json = {
"version": 1,
"blocks": [
{
"id": 23,
"action": "wolf actions",
"type": "nlogo:procedure",
"limit": 1,
"format": "to wolf-actions",
"blockColor": "#bb5555",
"required": true
},
{
"id": 24,
"action": "forward",
"format": "forward 10",
"type": "nlogo:command",
"params": [],
"properties": []
}
],
"expressions": [],
"program": {
"chains": [
[
{
"id": 23,
"action": "wolf actions",
"type": "nlogo:procedure",
"format": "to wolf-actions",
"required": true,
"x": 4,
"y": 8
},
{
"id": 24,
"action": "forward",
"type": "nlogo:command",
"format": "forward {0}",
"required": false,
"x": 4,
"y": 11.4,
"params": [
{
"id": 3,
"type": "range",
"name": "steps",
"unit": "",
"value": 1,
"default": 1,
"random": false,
"step": 0.1,
"min": 0,
"max": 3
}
]
}
]
]
}
}
var expected = copyJson(json)
expected.height = DEFAULT_HEIGHT
expected.width = DEFAULT_WIDTH
expected.blocks[1].required = false
expected.program.chains[0][0].instanceId = 0
expected.program.chains[0][0].limit = 1
expected.program.chains[0][0].blockColor = '#bb5555'
expected.program.chains[0][1].instanceId = 1
expected.program.chains[0][1].format = "forward 10"
delete expected.blocks[1].params
delete expected.blocks[1].properties
delete expected.expressions
delete expected.program.chains[0][1].params
expected = VersionManager.updateWorkspace(expected)
NetTango.restore("NetLogo", "nt-canvas", json, formatAttributeForNetTangoExtension, defaultOptions)
var result = NetTango.save("nt-canvas")
expect(result).toStrictEqual(expected)
var codeResult = NetTango.exportCode("nt-canvas", null)
expect(codeResult).toStrictEqual("to wolf-actions\n forward 10\nend\n\n")
})
test("NetLogo code exports in proper order with params", () => {
const testCanavsID = "nt-canvas"
var expected = copyJson(NETLOGO_MODEL_1)
expected.height = DEFAULT_HEIGHT
expected.width = DEFAULT_WIDTH
expected.blocks[1].propertiesDisplay = 'shown'
expected.blocks[1].required = false
expected.program.chains[0][0].blockColor = '#bb5555'
expected.program.chains[0][0].instanceId = 0
expected.program.chains[0][1].instanceId = 1
expected.program.chains[0][1].propertiesDisplay = 'shown'
expected.program.chains[1][0].blockColor = '#bb5555'
expected.program.chains[1][0].instanceId = 2
expected.program.chains[1][1].instanceId = 3
expected.program.chains[1][1].propertiesDisplay = 'shown'
delete expected.blocks[1].params[0].value
delete expected.blocks[1].properties[0].value
expected = VersionManager.updateWorkspace(expected)
NetTango.restore("NetLogo", testCanavsID, copyJson(NETLOGO_MODEL_1), formatAttributeForNetTangoExtension, defaultOptions)
const result = NetTango.save(testCanavsID)
expect(result).toStrictEqual(expected)
const codeResult = NetTango.exportCode(testCanavsID, null)
expect(codeResult).toStrictEqual("to sheep-actions\n forward (__nt-canvas_24_1_0 + __nt-canvas_24_1_P0)\nend\n\nto wolf-actions\n forward (__nt-canvas_24_0_0 + __nt-canvas_24_0_P0)\nend\n\n")
})
test("Model with ifelse properly imports and generates code", () => {
const testCanavsID = "nt-canvas"
const proc: any = { "id": 4, "action": "wolf actions", "format": "to wolf", "type": "nlogo:procedure", "required": true }
const forward: any = { "id": 1, "action": "forward", "format": "forward 1", "required": false }
const wiggle: any = { "id": 2, "action": "wiggle", "format": "left random 360", "required": false }
const chance: any = { "id": 0, "action": "chance", "format": "ifelse random 100 < 20", "type": "nlogo:ifelse", "required": false, "clauses": [ { "children": [] } ] }
const forwardInst = copyJson(forward)
forwardInst["instanceId"] = 2
const wiggleInst = copyJson(wiggle)
wiggleInst["instanceId"] = 3
const chanceInst = copyJson(chance)
chanceInst["instanceId"] = 1
chanceInst["children"] = [ forwardInst ]
chanceInst["clauses"] = [ { "children": [ wiggleInst ] }, { "action": "end-chance"} ]
const procInst = copyJson(proc)
procInst["instanceId"] = 0
const model = {
"version": 2,
"blocks": [ proc, chance, forward, wiggle ],
"program": { "chains": [ [ procInst, chanceInst ] ] }
}
var expected = copyJson(model)
expected.height = DEFAULT_HEIGHT
expected.width = DEFAULT_WIDTH
expected.program.chains[0][1].clauses = [{ "children": [copyJson(wiggleInst)]}]
expected = VersionManager.updateWorkspace(expected)
NetTango.restore("NetLogo", "nt-canvas", model, formatAttributeForNetTangoExtension, defaultOptions)
var result = NetTango.save("nt-canvas")
expect(result).toStrictEqual(expected)
var codeResult = NetTango.exportCode(testCanavsID, null)
expect(codeResult).toStrictEqual("to wolf\n ifelse random 100 < 20\n [\n forward 1\n ]\n [\n left random 360\n ]\nend\n\n")
})
test("Unversioned model gets IDs added for version 1", () => {
const action = {
"action": "sheep actions",
"required": false,
"params": [ { "type": "int", "default": 10, "step": 1 } ],
"properties": [ { "type": "int", "default": 9, "step": 1 } ]
}
const block = {
"action": "sheep actions",
"required": false,
"params": [ { "type": "int", "default": 5 } ],
"properties": [ { "type": "int", "default": 4 } ]
}
const model = {
"blocks": [ action ],
"program": { "chains": [ [ block ] ] }
}
var expected = copyJson(model)
expected.height = DEFAULT_HEIGHT
expected.width = DEFAULT_WIDTH
expected.blocks[0].propertiesDisplay = 'shown'
expected.program.chains[0][0].instanceId = 0
expected.program.chains[0][0].propertiesDisplay = 'shown'
expected.program.chains[0][0].params[0].default = 10
expected.program.chains[0][0].params[0].step = 1
expected.program.chains[0][0].params[0].value = 10
expected.program.chains[0][0].properties[0].default = 9
expected.program.chains[0][0].properties[0].step = 1
expected.program.chains[0][0].properties[0].value = 9
expected = VersionManager.updateWorkspace(expected)
NetTango.restore("NetLogo", "nt-canvas", model, formatAttributeForNetTangoExtension, defaultOptions)
const result = NetTango.save("nt-canvas")
expect(result).toStrictEqual(expected)
})
test("Duplicate menu block IDs get reset automatically", () => {
const model: any = {
"version": 5, "width": 300, "height": 300,
"blocks": [ {
"id": 0,
"action": "sheep actions", "required": true, "placement": BlockPlacement.STARTER,
"params": [ { "id": 0, "type": "int", "default": 10, "step": 2 } ],
"properties": [ { "id": 1, "type": "int", "default": 9, "step": 1 } ],
"propertiesDisplay": "shown"
},
{
"id": 0,
"action": "wolf actions", "required": true, "placement": BlockPlacement.STARTER,
"params": [ { "id": 0, "type": "int", "default": 10, "step": 2 } ],
"properties": [ { "id": 1, "type": "int", "default": 9, "step": 1 } ],
"propertiesDisplay": "shown"
}
],
"program": { "chains": [] }
}
var expected = copyJson(model)
expected.blocks[1].id = 1
expected = VersionManager.updateWorkspace(expected)
NetTango.restore("NetLogo", "nt-canvas", model, formatAttributeForNetTangoExtension, defaultOptions)
const result = NetTango.save("nt-canvas")
expect(result).toStrictEqual(expected)
})
test("Version 1 gets IDs added for new block", () => {
const action: any = {
"action": "sheep actions", "required": true,
"params": [ { "type": "int", "default": 10, "step": 1 } ],
"properties": [ { "type": "int", "default": 9, "step": 1 } ]
}
const model: any = {
"blocks": [ action ],
"program": { "chains": [] },
"version": 1
}
var expected = copyJson(model)
expected.height = DEFAULT_HEIGHT
expected.width = DEFAULT_WIDTH
expected.blocks[0].id = 0
expected.blocks[0].propertiesDisplay = 'shown'
expected.blocks[0].params[0].id = 0
expected.blocks[0].properties[0].id = 1
expected = VersionManager.updateWorkspace(expected)
NetTango.restore("NetLogo", "nt-canvas", model, formatAttributeForNetTangoExtension, defaultOptions)
const result = NetTango.save("nt-canvas")
expect(result).toStrictEqual(expected)
})
test("Version 3 model with select and expression attributes saves correctly", () => {
const model =
{
"version": 3,
"height": 500,
"width": 430,
"blocks": [
{
"action": "procede", "format": "to procede show ({0} + {P0})", "required": true, "limit": 1, "id": 0, "type": "nlogo:procedure",
"params": [
{
"name": "SelectMe", "type": "select", "default": "10",
"values": [ { "actual": "10", "display": "mars" }, { "actual": "20", "display": "venus" }, { "actual": "30", "display": "mercury" } ],
"id": 0
}
],
"properties": [ { "name": "ExpressMe", "type": "num", "default": "5", "id": 1 } ]
},
{
"action": "show", "format": "show ({P0})", "required": false, "id": 1, "type": "nlogo:command",
"properties": [ { "name": "ExpressMe", "type": "num", "default": "1", "id": 0 } ]
}
],
"expressions": [
{ "name": "true", "type": "bool" },
{ "name": "false", "type": "bool" },
{ "name": "AND", "type": "bool", "arguments": [ "bool", "bool" ], "format": "({0} and {1})" },
{ "name": "NOT", "type": "bool", "arguments": [ "bool" ], "format": "(not {0})" },
{ "name": ">", "type": "bool", "arguments": [ "num", "num" ] },
{ "name": "random", "type": "num", "arguments": [ "num" ], "format": "random-float {0}" }
],
"program": {
"chains": [
[
{
"id": 0, "instanceId": 2, "action": "procede", "type": "nlogo:procedure", "format": "to procede show ({0} + {P0})", "required": true, "x": 46, "y": 11,
"params": [
{
"id": 0, "type": "select", "name": "SelectMe", "value": "20", "default": "10",
"values": [ { "actual": "10", "display": "mars" }, { "actual": "20", "display": "venus" }, { "actual": "30", "display": "mercury" } ]
}
],
"properties": [
{
"id": 1, "type": "num", "name": "ExpressMe",
"value": { "name": "+", "type": "num",
"children": [
{ "name": "5", "type": "num" },
{ "name": "/", "type": "num",
"children": [
{ "name": "7", "type": "num" },
{ "name": "2", "type": "num" }
]
}
]
},
"default": "5",
"expressionValue": "(5 + (7 / 2))"
}
],
"propertiesDisplay": "hidden"
},
{
"id": 1, "instanceId": 6, "action": "show", "type": "nlogo:command", "format": "show ({P0})", "required": false,
"properties": [ { "id": 0, "type": "num", "name": "ExpressMe", "value": "3", "default": "1" } ],
"propertiesDisplay": "hidden"
}
]
]
}
}
var expected = copyJson(model)
expected.blocks[0].propertiesDisplay = 'shown'
expected.blocks[1].propertiesDisplay = 'shown'
expected.program.chains[0][0].instanceId = 0
expected.program.chains[0][1].instanceId = 1
expected = VersionManager.updateWorkspace(expected)
NetTango.restore("NetLogo", "nt-canvas", model, formatAttributeAsValue, defaultOptions)
const code = NetTango.exportCode("nt-canvas", null)
expect(code).toStrictEqual("to procede show (20 + (5 + (7 / 2)))\n show (3)\nend\n\n")
const result = NetTango.save("nt-canvas")
expect(result).toStrictEqual(expected)
})
test("Model uses custom clause open and close formats correctly", () => {
const model = {
"version": 5, "height": 600, "width": 450,
"blocks": [{
"id": 0,
"action": "to sheep-actions",
"required": true,
"placement": BlockPlacement.STARTER
},{
"id": 1,
"action": "ifelses",
"required": false,
"format": "(ifelse", "closeClauses": ") ; {0}",
"placement": BlockPlacement.CHILD,
"clauses": [
{ "children": [], "open": "random {0} > 10 [" },
{ "children": [], "open": "random {0} > 20 [", "close": "] ; {0}" },
{ "children": [] }
],
"params": [ { "id": 0, "type": "int", "name": "sample", "step": 1, "default": 10 } ]
},
{ "id": 2, "action": "show \"hello!\"", "required": false, "placement": BlockPlacement.CHILD }
],
"program": {
"chains": [{
"x": 10, "y": 10,
"blocks": [{
"id": 0,
"instanceId": 0,
"action": "to sheep-actions",
"required": true,
"placement": BlockPlacement.STARTER
}, {
"id": 1,
"instanceId": 1,
"action": "ifelses",
"required": false,
"format": "(ifelse", "closeClauses": ") ; {0}",
"placement": BlockPlacement.CHILD,
"clauses": [
{ "open": "random {0} > 10 [", "children": [{ "id": 2, "instanceId": 2, "action": "show \"hello!\"", "required": false, "placement": BlockPlacement.CHILD }]},
{ "open": "random {0} > 20 [", "close": "] ; {0}", "children": [{ "id": 2, "instanceId": 3, "action": "show \"hello!\"", "required": false, "placement": BlockPlacement.CHILD} ] },
{ "children": [{ "id": 2, "instanceId": 4, "action": "show \"hello!\"", "required": false, "placement": BlockPlacement.CHILD }] }
],
"params": [ { "id": 0, "type": "int", "name": "sample", "step": 1, "default": 10, "value": 15 } ]
}]
}]
}
}
var expected = copyJson(model)
expected = VersionManager.updateWorkspace(expected)
const containerId = "nt-canvas"
NetTango.restore("NetLogo", containerId, copyJson(model), formatAttributeAsValue, defaultOptions)
const result = NetTango.save(containerId)
expect(result).toStrictEqual(expected)
const codeResult = NetTango.exportCode(containerId, null)
const expectedCode =
`to sheep-actions
(ifelse
random 15 > 10 [
show "hello!"
]
random 15 > 20 [
show "hello!"
] ; 15
[
show "hello!"
]
) ; 15
end
`
expect(codeResult).toStrictEqual(expectedCode)
})