this repo has no description
1<!DOCTYPE html> 2<html> 3 <head> 4 <meta charset="utf-8"> 5 <title>MiniZinc Test Creator</title> 6 <link rel="stylesheet" href="https://unpkg.com/wingcss"> 7 <link rel="stylesheet" href="https://unpkg.com/codemirror@5.50.2/lib/codemirror.css"> 8 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 9 <script src="https://rawgit.com/farzher/fuzzysort/master/fuzzysort.js"></script> 10 <script src="https://unpkg.com/codemirror@5.50.2/lib/codemirror.js"></script> 11 <script src="https://cdn.jsdelivr.net/npm/vue-codemirror@4.0.0/dist/vue-codemirror.js"></script> 12 <script src="https://unpkg.com/codemirror@5.50.2/mode/yaml/yaml.js"></script> 13 <style> 14 .files, .cases { 15 position: fixed; 16 top: 0; 17 height: 100vh; 18 overflow-x: hidden; 19 overflow-y: auto; 20 } 21 22 .title { 23 padding: 1rem; 24 padding-bottom: 0; 25 } 26 27 button, li { 28 cursor: pointer; 29 } 30 31 .active { 32 font-weight: bold; 33 color: blue; 34 } 35 36 .top { 37 position: sticky; 38 top: 0; 39 left: 0; 40 padding: 1rem; 41 background: #f9f9f9; 42 border-bottom: solid 1px #CCC; 43 } 44 45 .top input { 46 margin: 0; 47 } 48 49 ul { 50 list-style: none; 51 margin: 0; 52 padding: 1rem; 53 } 54 55 li { 56 overflow: hidden; 57 text-overflow: ellipsis; 58 white-space: nowrap; 59 } 60 61 .files { 62 left: 0; 63 width: 300px; 64 } 65 66 .cases { 67 left: 300px; 68 width: 250px; 69 } 70 71 .main { 72 margin-left: 550px; 73 } 74 75 .CodeMirror { 76 border: 1px solid #CCC; 77 height: auto; 78 } 79 80 .expected { 81 margin-bottom: 1rem; 82 } 83 84 .remove { 85 margin: 0; 86 padding: 0.5rem 1rem; 87 } 88 89 .options { 90 display: flex; 91 align-items: center; 92 justify-content: flex-start; 93 } 94 95 .options div { 96 padding-right: 3rem; 97 } 98 99 .options input, .options button { 100 margin: 0.5rem 0 !important; 101 } 102 </style> 103 </head> 104 <body> 105 <div id="app"> 106 <div class="files"> 107 <div class="row top"><input type="text" placeholder="Search" v-model="search"></div> 108 <ul> 109 <li v-for="file in filteredFiles" :title="file" :class="{ active: activeFile === file }" v-on:click="chooseFile(file)">{{ modified[file] ? '● ' : '' }}{{file}}</button> 110 </ul> 111 </div> 112 <div v-if="cases" class="cases"> 113 <div class="title"><strong>Edit test case</strong></div> 114 <ul> 115 <li v-for="(c, i) in cases" :class="{ active: activeTest === i }" v-on:click="chooseTest(i)">{{ c.name ? c.name : `Test case ${i+1}`}}</li> 116 </ul> 117 <div class="center"> 118 <button class="col" v-on:click="addCase()">Add new test case</button> 119 </div> 120 <div v-if="modified[activeFile]" class="center"> 121 <button class="col" v-on:click="save()">Save changes</button> 122 </div> 123 </div> 124 <div v-if="testCase" class="main"> 125 <div class="row"> 126 <fieldset class="col"> 127 <legend>Test Case</legend> 128 <div class="row"> 129 <input type="text" placeholder="Test name" v-model="testCase.name"/> 130 </div> 131 <div class="row"> 132 <textarea placeholder="Extra (.dzn) files, one per line" v-model="testCase.extra_files"></textarea> 133 </div> 134 <div class="row"> 135 <fieldset class="col"> 136 <legend>Solvers</legend> 137 <div class="row"> 138 <div> 139 <label><input type="checkbox" value="gecode" v-model="testCase.solvers">Gecode</label> 140 <label><input type="checkbox" value="chuffed" v-model="testCase.solvers">Chuffed</label> 141 <label><input type="checkbox" value="cbc" v-model="testCase.solvers">CBC</label> 142 </div> 143 </div> 144 </fieldset> 145 <fieldset v-if="testCase.type === 'solve'" class="col"> 146 <legend>Checkers</legend> 147 <div class="row"> 148 <div> 149 <label><input type="checkbox" value="gecode" v-model="testCase.check_against">Gecode</label> 150 <label><input type="checkbox" value="chuffed" v-model="testCase.check_against">Chuffed</label> 151 <label><input type="checkbox" value="cbc" v-model="testCase.check_against">CBC</label> 152 </div> 153 </div> 154 </fieldset> 155 <fieldset class="col"> 156 <legend>Type</legend> 157 <div class="row"> 158 <div> 159 <label><input type="radio" value="solve" v-model="testCase.type">Solve</label> 160 <label><input type="radio" value="compile" v-model="testCase.type">Compile</label> 161 </div> 162 </div> 163 </fieldset> 164 </div> 165 <div class="row"> 166 <fieldset class="col"> 167 <legend>Options</legend> 168 <div v-if="testCase.type === 'solve'" class="row"> 169 <label><input type="checkbox" v-model="testCase.all_solutions">All solutions</label> 170 </div> 171 <div class="row"> 172 <label class="horizontal-align vertical-align"> 173 <span>Timeout&nbsp;(seconds):&nbsp;&nbsp;</span> 174 <input type="number" step="any" min="0" v-model="testCase.timeout"> 175 </label> 176 </div> 177 <div> 178 <fieldset> 179 <legend>Extra command line options</legend> 180 <div class="options" v-for="(opt, i) in testCase.options"> 181 <div ><input type="text" v-model="opt.key" placeholder="Option/flag"></div > 182 <div><input type="text" v-model="opt.value" placeholder="Value (or empty if flag)"></div> 183 <div><button v-on:click="testCase.options = [...testCase.options.slice(0, i), ...testCase.options.slice(i + 1)]">Remove</button></div> 184 </div> 185 <button v-on:click="testCase.options = [...testCase.options, {key: '', value: ''}]">Add option</button> 186 </fieldset> 187 </div> 188 </fieldset> 189 </div> 190 <div class="row"> 191 <fieldset class="col"> 192 <legend>Expected Output</legend> 193 <div class="row"> 194 <div class="col"> 195 <fieldset v-if="testCase.type === 'solve'"> 196 <legend>Generate</legend> 197 <div class="row"> 198 <div> 199 <label><input type="radio" value="include" v-model="mode">Include variables</label> 200 <label><input type="radio" value="exclude" v-model="mode">Exclude variables</label> 201 </div> 202 <textarea placeholder="Variable names, one per line" v-model="variables"></textarea> 203 </div> 204 <div> 205 <button v-on:click="generate()">Generate</button> 206 </div> 207 </fieldset> 208 <div class="col"> 209 <div v-for="(expected, i) in testCase.expected" class="expected"> 210 <div class="right"><button class="remove" v-on:click="testCase.expected = [...testCase.expected.slice(0, i), ...testCase.expected.slice(i+1)]">Remove</button></div> 211 <codemirror v-model="expected.value" :options="{mode: 'yaml', tabSize: 2, styleActiveLine: true, lineNumbers: true}"></codemirror> 212 </div> 213 <div> 214 <button v-on:click="testCase.expected = [...testCase.expected, {value: ''}]">Add new expected output</button> 215 <button v-if="testCase.expected.length > 0" v-on:click="testCase.expected = []" class="outline">Clear expected outputs</button> 216 </div> 217 </div> 218 </div> 219 </div> 220 </fieldset> 221 </div> 222 <div class="row"> 223 <div> 224 <button v-on:click="deleteCase()">Delete test case</button> 225 </div> 226 </div> 227 </fieldset> 228 </div> 229 </div> 230 </div> 231 232 <script> 233 Vue.use(VueCodemirror); 234 const app = new Vue({ 235 el: '#app', 236 data() { 237 return { 238 files: [], 239 activeFile: null, 240 activeTest: null, 241 loaded: {}, 242 modified: {}, 243 search: '', 244 justSwitchedFiles: false, 245 246 mode: 'exclude', 247 variables: '' 248 }; 249 }, 250 async mounted() { 251 const response = await fetch('/files.json'); 252 const json = await response.json(); 253 this.files = json.files; 254 this.searchTerms = json.files.map(x => fuzzysort.prepare(x)); 255 }, 256 watch: { 257 async activeFile(file) { 258 if (file && !(file in this.loaded)) { 259 const response = await fetch(`/file.json?f=${encodeURIComponent(file)}`); 260 const json = await response.json(); 261 this.loaded = { ...this.loaded, [file]: json.cases }; 262 } 263 this.activeTest = 0; 264 this.justSwitchedFiles = true; 265 }, 266 cases: { 267 deep: true, 268 handler(val, oldVal) { 269 if (this.justSwitchedFiles) { 270 this.justSwitchedFiles = false; 271 return; 272 } 273 274 if (!val || !oldVal) 275 return; 276 277 this.modified = { ...this.modified, [this.activeFile]: true }; 278 } 279 }, 280 testCase: { 281 deep: true, 282 handler(testCase) { 283 if (testCase && testCase.type === 'compile') { 284 if (testCase.check_against.length > 0) 285 testCase.check_against = []; 286 if (testCase.all_solutions) 287 testCase.all_solutions = false; 288 } 289 } 290 } 291 }, 292 computed: { 293 filteredFiles() { 294 if (this.search.length === 0) 295 return this.files; 296 return fuzzysort 297 .go(this.search, this.searchTerms, { limit: 20 }) 298 .map(x => x.target); 299 }, 300 cases() { 301 if (this.activeFile === null || !(this.activeFile in this.loaded)) 302 return null; 303 return this.loaded[this.activeFile]; 304 }, 305 testCase() { 306 return this.cases && this.cases[this.activeTest]; 307 } 308 }, 309 methods: { 310 chooseFile(file) { 311 this.activeFile = file; 312 }, 313 chooseTest(i) { 314 this.activeTest = i; 315 }, 316 addCase() { 317 this.loaded[this.activeFile] = [...this.cases, { 318 name: '', 319 solvers: ['gecode', 'chuffed', 'cbc'], 320 check_against: [], 321 expected: [], 322 all_solutions: false, 323 timeout: '', 324 options: [], 325 extra_files: '', 326 markers: [], 327 type: 'solve' 328 }]; 329 this.chooseTest(this.cases.length - 1); 330 }, 331 deleteCase() { 332 this.loaded[this.activeFile] = [...this.cases.slice(0, this.activeTest), ...this.cases.slice(this.activeTest+1)]; 333 this.activeTest = Math.min(this.activeTest, this.cases.length - 1); 334 }, 335 async save() { 336 const response = await fetch(`/save.json?f=${encodeURIComponent(this.activeFile)}`, { 337 method: 'POST', 338 headers: { 339 'Content-Type': 'application/json' 340 }, 341 body: JSON.stringify(this.cases) 342 }); 343 344 const json = await response.json(); 345 346 if (json.status === 'success') { 347 this.modified = { ...this.modified, [this.activeFile]: false }; 348 } 349 }, 350 async generate() { 351 const response = await fetch(`/generate.json?f=${encodeURIComponent(this.activeFile)}&mode=${encodeURIComponent(this.mode)}&vars=${encodeURIComponent(this.variables)}`, { 352 method: 'POST', 353 headers: { 354 'Content-Type': 'application/json' 355 }, 356 body: JSON.stringify(this.testCase) 357 }); 358 359 const json = await response.json(); 360 if (json.obtained.length > 0) 361 this.testCase.expected = [...this.testCase.expected, ...json.obtained]; 362 } 363 } 364 }); 365 366 window.onbeforeunload = function (e) { 367 e = e || window.event; 368 if (Object.values(app.modified).some(x => x)) { 369 if (e) 370 e.returnValue = 'Discard changes and exit?'; 371 return 'Discard changes and exit?'; 372 } 373 }; 374 </script> 375 </body> 376</html>