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 (seconds): </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>