| <html> |
| <head> |
| <meta http-equiv="Content-type" content="text/html; charset=utf-8"> |
| <title>Touchpad Log Viewer</title> |
| <link type="text/css" href="css/smoothness/jquery-ui-1.8.16.custom.css" rel="stylesheet" /> |
| <script src="js/jquery-1.6.4.min.js"></script> |
| <script src="js/jquery.mousewheel.js"></script> |
| <script src="js/jquery-ui-1.8.16.custom.min.js"></script> |
| <script src="js/bzip2.js"></script> |
| <script src="js/base64-binary.js"></script> |
| <script src="js/jsxcompressor.js"></script> |
| <script src="js/purl.js"></script> |
| |
| <script src="finger_view_controller.js"></script> |
| <script src="graph_controller.js"></script> |
| <script src="secret_entries.js"></script> |
| |
| |
| <link rel="stylesheet" href="/style.css" type="text/css" media="all" /> |
| <script type="text/javascript" charset="utf-8"> |
| |
| var finger_view_controller; |
| var current_layer = 0; |
| var json_obj; |
| |
| var use_server = false; |
| var logname = undefined; |
| |
| function update_range(event, ui) { |
| finger_view_controller.setRange(ui.values[0], ui.values[1]); |
| var begin_event = finger_view_controller.getEvent(ui.values[0]); |
| $("#begin_event").val(begin_event); |
| if ((beginTimestamp = finger_view_controller.getTimestamp(ui.values[0])) > 0) |
| $('#input_begin_time').val(beginTimestamp); |
| var end_event = finger_view_controller.getEvent(ui.values[1]); |
| $("#end_event").val(end_event); |
| endTimestamp = finger_view_controller.getPreviousHardwareStateTimestamp( |
| ui.values[1]); |
| if (endTimestamp > 0) |
| $('#input_end_time').val(endTimestamp); |
| var values = $("#slider").slider("option", "values"); |
| $('#num_fts').text('Finger Touch Section (' + |
| finger_view_controller.getFTSIndex(values[1]) + ' / ' + |
| '0 ~ ' + (finger_view_controller.getNumFTS() - 1) + '): '); |
| } |
| |
| function loadLogObj(obj, isNew) { |
| if (!isNew) { |
| var values = $("#slider").slider("option", "values"); |
| var beginTime = finger_view_controller.getGETimestamp(values[0]); |
| var endTime = finger_view_controller.getLETimestamp(values[1]); |
| } |
| finger_view_controller.setEntriesLog(obj, current_layer); |
| finger_view_controller.initFTS(); |
| var MAX_SIZE = 10000; |
| var min = Math.max(0, finger_view_controller.entries.length - MAX_SIZE); |
| var max = finger_view_controller.entries.length - 1; |
| if (isNew) { |
| var values = [min, max]; |
| } else { |
| values[0] = finger_view_controller.getHardwareStateGETimestamp(beginTime); |
| values[1] = finger_view_controller.getHardwareStateLETimestamp(endTime); |
| } |
| $("#slider").slider({ |
| min: min, |
| max: max, |
| values: values, |
| range: true, |
| slide: update_range, |
| change: update_range |
| }); |
| update_range(null, { values: values }); |
| finger_view_controller.resetZooms(); |
| } |
| |
| /** |
| * Converts raw input event stream from evdev input to the entries structure |
| * expected by finger_view_controller.js. |
| * @param {string} data The raw log file data in the following format: |
| * # Metadata in comment lines. |
| * # absinfo: [event_type] [min] [max] [fuzz] [flat] [resolution] |
| * E: [timestamp] [event_type] [event_code] [event_value] [slot] |
| * @return {Object} Event structure expected by loadLogObj. |
| */ |
| function convertEventStreamToEntries(data) { |
| // Clone an object by serializing and parsing it. |
| var clone = function(obj) { |
| return JSON.parse(JSON.stringify(obj)); |
| } |
| |
| // Default hardware properties. |
| var obj = { |
| "entries": [], |
| "hardwareProperties": { |
| "bottom": 1000.0, |
| "left": 0.0, |
| "right": 2000.0, |
| "top": 0.0, |
| "xResolution": 1.0, |
| "yResolution": 1.0}, |
| "properties": { |
| "Pressure Calibration Offset": 0.0, |
| "Pressure Calibration Slope": 1.0, |
| }}; |
| var evt = { |
| "fingers": [], |
| "timestamp": 0, |
| "touchCount": 0, |
| "type": "hardwareState", |
| }; |
| var eventCodes = { |
| '002f': 'slot', |
| '0030': 'touchMajor', |
| '0035': 'positionX', |
| '0036': 'positionY', |
| '0039': 'trackingId', |
| '003a': 'pressure', |
| }; |
| var ignoreCodes = { |
| '0000': 'abs_x', |
| '0001': 'abs_y', |
| '0018': 'abs_pressure', |
| }; |
| var slotDefaults = { |
| 'flags': 0, |
| 'trackingId': -1, |
| }; |
| var slots = []; |
| var slot = -1; |
| var lines = data.split('\n'); |
| for (var i = 0; i < lines.length; i++) { |
| if (lines[i].slice(0, 10) == '# absinfo:') { |
| var val = lines[i].split(' '); |
| switch (val[2]) { |
| case '0': |
| obj.hardwareProperties.left = parseInt(val[3]); |
| obj.hardwareProperties.right = parseInt(val[4]) + 1; |
| obj.hardwareProperties.xResolution = parseInt(val[7]); |
| break; |
| case '1': |
| obj.hardwareProperties.top = parseInt(val[3]); |
| obj.hardwareProperties.bottom = parseInt(val[4]) + 1; |
| obj.hardwareProperties.yResolution = parseInt(val[7]); |
| break; |
| case '47': // Slot. |
| var maxSlot = parseInt(val[4]); |
| while (slots.length <= maxSlot) |
| slots.push(clone(slotDefaults)); |
| break; |
| } |
| } else if (lines[i].slice(0, 3) == 'E: ') { |
| var val = lines[i].split(' '); |
| switch (val[2]) { // Event type. |
| case '0000': // EV_SYN: Clone current hardware state and push to list. |
| // First copy slots into fingers. |
| evt.fingers = []; |
| for (var j = 0; j < slots.length; j++) { |
| if (slots[j].trackingId >= 0) |
| evt.fingers.push(slots[j]); |
| } |
| evt.touchCount = evt.fingers.length; |
| evt.timestamp = parseFloat(val[1]); |
| obj.entries.push(clone(evt)); |
| break; |
| case '0003': // EV_ABS: Update hardware state. |
| if (eventCodes[val[3]]) { |
| // Parse slot identifier on event info line. |
| if (val.length > 5) |
| slot = parseInt(val[5]); |
| |
| if (eventCodes[val[3]] == 'slot') { |
| slot = parseInt(val[4]); |
| } else if (slot >= 0) { |
| // Initialize the requested slot if not set. |
| if (!slots[slot]) |
| slots[slot] = clone(slotDefaults); |
| |
| // Make up tracking ID if we miss the start of a touch. |
| if (eventCodes[val[3]] != 'trackingId' && |
| slots[slot].trackingId == -1) |
| slots[slot].trackingId = slot + 1; |
| |
| slots[slot][eventCodes[val[3]]] = parseInt(val[4]); |
| } |
| } else if (!ignoreCodes[val[3]]) { |
| console.log('Unrecognized event type ' + val[2] + |
| ' code ' + val[3]); |
| } |
| break; |
| } |
| } |
| } |
| // For touch screens the absolute value is more useful than the scaled value. |
| obj.hardwareProperties.xResolution = 1.0; |
| obj.hardwareProperties.yResolution = 1.0; |
| return obj; |
| } |
| |
| function loadLog(obj) { |
| if (!obj) { |
| alert('Unrecognized input file'); |
| return; |
| } |
| json_obj = obj; |
| generateRadioButtons(obj); |
| current_layer = 0; |
| loadLogObj(obj, true); |
| } |
| |
| function handleFileSelect(evt) { |
| $('#text_box').removeClass('text_box_hilight'); |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| |
| var files = evt.dataTransfer.files; // FileList object. |
| |
| var file = files[0]; |
| var reader = new FileReader(); |
| reader.onload = function(e) { |
| var obj; |
| $('#text_box').val(e.target.result); |
| if (e.target.result[0] == '#') |
| obj = convertEventStreamToEntries(e.target.result); |
| else if (e.target.result[0] == '{') |
| obj = jQuery.parseJSON(e.target.result); |
| loadLog(obj); |
| }; |
| reader.readAsText(file); |
| } |
| |
| var count = 0; |
| |
| function handleDragOver(evt) { |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| } |
| |
| function handleDragEnter(evt) { |
| $('#text_box').addClass('text_box_hilight'); |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| } |
| |
| function handleDragOut(evt) { |
| $('#text_box').removeClass('text_box_hilight'); |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| } |
| |
| function setup() { |
| var dropZone = document.getElementById('text_box'); |
| dropZone.addEventListener('dragenter', handleDragEnter, false); |
| dropZone.addEventListener('dragleave', handleDragOut, false); |
| dropZone.addEventListener('dragover', handleDragOver, false); |
| dropZone.addEventListener('drop', handleFileSelect, false); |
| } |
| |
| // fix layerX, layerY warnings |
| (function(){ |
| // remove layerX and layerY |
| var all = $.event.props, |
| len = all.length, |
| res = []; |
| while (len--) { |
| var el = all[len]; |
| if (el != 'layerX' && el != 'layerY') res.push(el); |
| } |
| $.event.props = res; |
| }()); |
| |
| function begin_stepBack() { |
| var values = $("#slider").slider("option", "values"); |
| var minValue = $("#slider").slider("option", "min"); |
| if (values[0] > minValue) { |
| var newValue = finger_view_controller.prevHardwareState(values[0]); |
| if (newValue < 0) |
| return; |
| values[0] = newValue; |
| $("#slider").slider("option", "values", values); |
| $('#input_begin_time').val(finger_view_controller.getTimestamp(newValue)); |
| } |
| } |
| |
| function begin_stepForward() { |
| var values = $("#slider").slider("option", "values"); |
| if (values[0] < values[1]) { |
| var newValue = finger_view_controller.nextHardwareState(values[0]); |
| if (newValue < 0) |
| return; |
| values[0] = newValue; |
| $("#slider").slider("option", "values", values); |
| $('#input_begin_time').val(finger_view_controller.getTimestamp(newValue)); |
| } |
| } |
| |
| function end_stepBack() { |
| var values = $("#slider").slider("option", "values"); |
| if (values[1] > values[0]) { |
| var newValue = finger_view_controller.prevHardwareState(values[1]); |
| if (newValue < 0) |
| return; |
| values[1] = newValue; |
| $("#slider").slider("option", "values", values); |
| $('#input_end_time').val(finger_view_controller.getTimestamp(newValue)); |
| } |
| } |
| |
| function end_stepForward() { |
| var values = $("#slider").slider("option", "values"); |
| var maxValue = $("#slider").slider("option", "max"); |
| if (values[1] < maxValue) { |
| var newValue = finger_view_controller.nextHardwareState(values[1]); |
| if (newValue < 0) |
| return; |
| values[1] = newValue; |
| $("#slider").slider("option", "values", values); |
| $('#input_end_time').val(finger_view_controller.getTimestamp(newValue)); |
| } |
| } |
| |
| function setBeginTimestamp() { |
| timestamp = $('#input_begin_time').val(); |
| var values = $('#slider').slider('option', 'values'); |
| var minValue = $('#slider').slider('option', 'min'); |
| var maxValue = $('#slider').slider('option', 'max'); |
| var newValue = finger_view_controller.getHardwareStateLETimestamp(timestamp); |
| if (newValue >= minValue && newValue <= maxValue) { |
| values[0] = (newValue >= values[1] ? values[1] : newValue); |
| $('#slider').slider('option', 'values', values); |
| } |
| } |
| |
| function setEndTimestamp() { |
| timestamp = $('#input_end_time').val(); |
| var values = $('#slider').slider('option', 'values'); |
| var minValue = $('#slider').slider('option', 'min'); |
| var maxValue = $('#slider').slider('option', 'max'); |
| var newValue = finger_view_controller.getHardwareStateGETimestamp(timestamp); |
| if (newValue >= minValue && newValue <= maxValue) { |
| values[1] = (newValue <= values[0] ? values[0] : newValue); |
| $('#slider').slider('option', 'values', values); |
| } |
| } |
| |
| function gotoPrevFTS() { |
| var values = $('#slider').slider('option', 'values'); |
| var fts = finger_view_controller.getPrevFTS(values); |
| $('#slider').slider('option', 'values', fts); |
| } |
| |
| function gotoNextFTS() { |
| var values = $('#slider').slider('option', 'values'); |
| var fts = finger_view_controller.getNextFTS(values); |
| $('#slider').slider('option', 'values', fts); |
| } |
| |
| function gotoFirstFTS() { |
| var fts = finger_view_controller.getFirstFTS(); |
| $('#slider').slider('option', 'values', fts); |
| } |
| |
| function gotoLastFTS() { |
| var fts = finger_view_controller.getLastFTS(); |
| $('#slider').slider('option', 'values', fts); |
| } |
| |
| function expandAllFTS() { |
| var values = finger_view_controller.getAllEntries(); |
| $('#slider').slider('option', 'values', values); |
| } |
| |
| |
| function shrink() { |
| var values = $("#slider").slider("option", "values"); |
| var snippet = finger_view_controller.getSnippet(values[0], values[1]); |
| $('#text_box').val(JSON.stringify(snippet, null, 2)); |
| loadLogObj(snippet, true); |
| |
| if (use_server) { |
| $.post('/save/' + logname, JSON.stringify(snippet, null, 2), function () { |
| alert("Saved"); |
| }); |
| } |
| } |
| |
| function generateRadioButtons(obj) { |
| var radioHTML = '<input type="radio" name="viewLayer" value="0" ' + |
| 'checked="checked" onclick="radioChange(event)">' + |
| obj.interpreterName + '</input><br/>'; |
| var layer = 0; |
| while (obj.hasOwnProperty('nextLayer')) { |
| obj = obj.nextLayer; |
| layer++; |
| radioHTML += '<input type="radio" name="viewLayer" value="' + layer + |
| '" onclick="radioChange(event)">' + obj.interpreterName + |
| '</input><br/>'; |
| } |
| document.getElementById('button_div').innerHTML = radioHTML; |
| if (layer > 1) { |
| $("#button_div").css("visibility", "visible"); |
| } else { |
| $("#button_div").css("visibility", "hidden"); |
| } |
| } |
| |
| function generateUnittest() { |
| var interpreter_name = $('#unittest_interpreter').val(); |
| var unittest_name = $('#unittest_testname').val(); |
| |
| var values = $('#slider').slider('option', 'values'); |
| var unittest = finger_view_controller.getUnitTest( |
| values[0], values[1], interpreter_name, unittest_name); |
| $('#unittest_box').val(unittest); |
| } |
| |
| function radioChange(e) { |
| if (e.target.value != current_layer) { |
| current_layer = e.target.value; |
| loadLogObj(json_obj, false); |
| } |
| } |
| |
| function extractActivityLog(tar) { |
| // a tar file is a sequence of 512 byte blocks. Starting with a header block |
| // followed by the contents of that file, repeated for as many files |
| // as are in the archive. The file contents are padded if zeros to fill up |
| // multiples of 512 byte blocks. |
| |
| // extracts name from tar file header |
| function getName(header_offset) { |
| name_array = new Uint8Array(tar.slice(header_offset, header_offset + 100)); |
| if (name_array[0] == 0) { |
| return undefined; |
| } |
| return String.fromCharCode.apply(null, name_array); |
| } |
| |
| // extracts length from tar file header |
| function getLength(header_offset) { |
| start = header_offset + 124; |
| length_array = new Uint8Array(tar.slice(start, start + 12)); |
| length_string = String.fromCharCode.apply(null, length_array); |
| return parseInt(length_string, 8); |
| } |
| |
| |
| // iterate over all files and collect all activity logs |
| var header_offset = 0; |
| var log_list = new Array(); |
| var name_pattern = /touchpad_activity_([0-9\-]+)/; |
| while (header_offset + 512 < tar.byteLength) { |
| |
| // cannot read a name from the header? -> End of archive. |
| var name = getName(header_offset); |
| if (name == undefined) |
| break; |
| |
| var length = getLength(header_offset); |
| |
| if (name_pattern.test(name)) { |
| // store file info in list |
| result = new Object(); |
| result.name = name; |
| result.timestamp = name_pattern.exec(name)[1]; |
| result.offset = header_offset + 512 |
| result.length = length; |
| log_list.push(result) |
| } |
| |
| // Next header starts after the file on the next 512 byte aligned |
| // block. |
| header_offset = Math.ceil((header_offset+length) / 512 + 1) * 512; |
| } |
| |
| // sort by descending timestamps to find latest file |
| log_list.sort(function(a, b) { |
| return a.timestamp < b.timestamp; |
| }); |
| log = log_list[0]; |
| return tar.slice(log.offset, log.offset + log.length); |
| } |
| |
| $(document).ready(function() { |
| setup(); |
| $("#playpause").button({ |
| text: false, |
| icons: { |
| primary: "ui-icon-play" |
| } |
| }); |
| var canvas = document.getElementById('fview'); |
| var gc = new GraphController($('#gview')); |
| gc.setLineSegments([{'start': {'xPos': 0.1, 'yPos': 0.1}, |
| 'end': {'xPos': 0.9, 'yPos': 0.5}}]); |
| var inGc = new GraphController($('#fview')); |
| finger_view_controller = new FingerViewController(inGc, gc, $('#intext'), |
| $('#out-lock-head')); |
| $("#out-resetzoom").button({ |
| icons: { |
| primary: "ui-icon-arrow-4-diag" |
| } |
| }).click(function() { |
| gc.animResetZoom(); |
| }); |
| |
| $("#in-resetzoom").button({ |
| icons: { |
| primary: "ui-icon-arrow-4-diag" |
| } |
| }).click(function() { |
| inGc.animResetZoom(); |
| }); |
| |
| $("#begin_stepback").button({ |
| text: false, |
| icons: { |
| primary: "ui-icon-triangle-1-w" |
| } |
| }).click(begin_stepBack); |
| $("#begin_stepforward").button({ |
| text: false, |
| icons: { |
| primary: "ui-icon-triangle-1-e" |
| } |
| }).click(begin_stepForward); |
| $("#end_stepback").button({ |
| text: false, |
| icons: { |
| primary: "ui-icon-triangle-1-w" |
| } |
| }).click(end_stepBack); |
| $("#end_stepforward").button({ |
| text: false, |
| icons: { |
| primary: "ui-icon-triangle-1-e" |
| } |
| }).click(end_stepForward); |
| |
| $("#shrink").button({ |
| text: true, |
| }).click(shrink); |
| $("#prev_finger_touch").button({ |
| text: true, |
| }).click(gotoPrevFTS); |
| $("#next_finger_touch").button({ |
| text: true, |
| }).click(gotoNextFTS); |
| $("#first_finger_touch").button({ |
| text: true, |
| }).click(gotoFirstFTS); |
| $("#last_finger_touch").button({ |
| text: true, |
| }).click(gotoLastFTS); |
| $("#all_finger_touch").button({ |
| text: true, |
| }).click(expandAllFTS); |
| $("#unittest").button({ |
| text: true, |
| }).click(generateUnittest); |
| |
| $(window).keypress(function (event) { |
| switch (event.keyCode) { |
| case 44: // ',' move the slider to previous fts |
| gotoPrevFTS(); |
| break; |
| case 46: // '.' move the slider to next fts |
| gotoNextFTS(); |
| break; |
| case 109: // 'm' move the slider to the first fts |
| gotoFirstFTS(); |
| break; |
| case 47: // '/' move the slider to the last fts |
| gotoLastFTS(); |
| break; |
| case 97: // 'a' expand the slider to all entries |
| expandAllFTS(); |
| break; |
| case 107: // 'k' |
| end_stepBack(); |
| break; |
| case 106: // 'j' |
| end_stepForward(); |
| break; |
| case 103: // 'g' |
| begin_stepBack(); |
| break; |
| case 104: // 'h' |
| begin_stepForward(); |
| break; |
| case 115: // 's' |
| shrink(); |
| break; |
| } |
| }); |
| |
| $('#bzipupload').change(function () { |
| // read selected file |
| reader = new FileReader(); |
| reader.onload = function (event) { |
| |
| // extract feedback.bz2 |
| var feedback_bz2 = new Uint8Array(event.target.result); |
| var feedback = bzip2.simple(bzip2.array(feedback_bz2)); |
| |
| // extract touchpad_activity_log.tar' from feedback |
| var start_key = 'hack-33025-touchpad_activity="""\nbegin-base64 644 touchpad_activity_log.tar'; |
| var start_idx = feedback.indexOf(start_key) + start_key.length + 1; |
| var end_idx = feedback.indexOf('====', start_idx) -1; |
| var touchpad_tar_base64 = feedback.substring(start_idx, end_idx); |
| |
| // decode base64 |
| var touchpad_tar = Base64Binary.decodeArrayBuffer(touchpad_tar_base64); |
| |
| // extract log.gz from touchpad.tar |
| var log_gz = extractActivityLog(touchpad_tar); |
| |
| // extract log.gz |
| var log = (new JXG.Util.Unzip(new Uint8Array(log_gz))).unzip()[0][0]; |
| |
| // parse as json and load |
| var log_obj = jQuery.parseJSON(log); |
| loadLog(log_obj); |
| }; |
| reader.readAsArrayBuffer(document.getElementById('bzipupload').files[0]); |
| }); |
| |
| logname = $.url().fparam('id'); |
| use_server = logname !== ''; |
| |
| if (use_server) { |
| $.get('/load/' + logname, function(data) { |
| loadLog(data); |
| }, 'json'); |
| } else { |
| loadLog(secret_obj); |
| } |
| }); |
| |
| </script> |
| </head> |
| <body> |
| <div id="container"> |
| <h2>Touchpad Activity Viewer</h2> |
| <div id="viewer"> |
| <div class="viewspan"> |
| <h3>Input</h3> |
| <canvas class="view" id="fview" width="480" height="320"></canvas><br/> |
| <button class="button" id="in-resetzoom">Reset Zoom</button> |
| <div id="intext">Time: 123123123.123<br/>Finger Cnt: 2<br/>Touch Cnt: 2</div> |
| </div> |
| <div class="viewspan"> |
| <h3>Output</h3> |
| <canvas class="view" id="gview" width="480" height="320"></canvas><br/> |
| <button class="button" id="out-resetzoom">Reset Zoom</button> |
| <input type="checkbox" id="out-lock-head" /> |
| <label for="out-lock-head">Lock Head Location</label> |
| </div> |
| <div class="midviewspan" id="button_div"></div> |
| </div> |
| <div id="playbar"> |
| <h2>Time Range</h2> |
| <button id="playpause">Play</button> |
| <button id="shrink">Shrink Range</button> |
| <div id="sliderdiv"><div id="slider"></div></div> |
| <div id="begin"> |
| <h3>Begin Time</h3> |
| <button id="begin_stepback">Begin Step Back</button> |
| <button id="begin_stepforward">Begin Step Forward</button> |
| Set begin time: <input type='text' id='input_begin_time' |
| onkeyup="setBeginTimestamp()" /> |
| <br/><textarea rows="4" id="begin_event" readonly="readonly"></textarea> |
| </div> |
| <div id="end"> |
| <h3>End Time</h3> |
| <button id="end_stepback">End Step Back</button> |
| <button id="end_stepforward">End Step Forward</button> |
| Set end time: <input type='text' id='input_end_time' |
| onkeyup="setEndTimestamp()" /> |
| <br/><textarea rows="4" id="end_event" readonly="readonly"></textarea> |
| </div> |
| <div id="touchselect"> |
| <span id='num_fts'></span> |
| <button id="first_finger_touch"> << </button> |
| <button id="prev_finger_touch"> < </button> |
| <button id="all_finger_touch"> all </button> |
| <button id="next_finger_touch"> > </button> |
| <button id="last_finger_touch"> >> </button> |
| </div> |
| </div> |
| <div id="load"> |
| <h2>Load/Save Activity Log</h2> |
| |
| <div id="textdiv"> |
| <h3>From Text File</h3> |
| <textarea id="text_box">Drag and drop a touchpad log file here.</textarea></div> |
| <div id="feedbackupload"> |
| <h3>From Feedback Report</h3> |
| Load from Feedback Report: <input type="file" id="bzipupload"></input> |
| </div> |
| <br style="clear: both;"/> |
| </div> |
| <div id="unittest_div"> |
| <input id="unittest_interpreter" value="InterpreterName"/> |
| <input id="unittest_testname" value="TestName"/> |
| <button id="unittest">Generate Unittest</button> |
| <textarea rows="24" cols="80" id="unittest_box"></textarea> |
| </div> |
| |
| </div> |
| </body> |
| </html> |