syd 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syd/__init__.py +3 -3
- syd/flask_deployment/__init__.py +1 -0
- syd/flask_deployment/components.py +376 -363
- syd/flask_deployment/deployer.py +247 -283
- syd/flask_deployment/static/css/viewer.css +82 -0
- syd/flask_deployment/static/js/viewer.js +174 -0
- syd/flask_deployment/templates/base.html +23 -20
- syd/flask_deployment/templates/viewer.html +49 -95
- syd/flask_deployment/testing_principles.md +300 -0
- syd/notebook_deployment/__init__.py +1 -1
- syd/notebook_deployment/_ipympl_deployer.py +258 -0
- syd/notebook_deployment/deployer.py +126 -50
- syd/notebook_deployment/widgets.py +142 -69
- syd/parameters.py +93 -180
- syd/plotly_deployment/__init__.py +1 -0
- syd/plotly_deployment/components.py +531 -0
- syd/plotly_deployment/deployer.py +376 -0
- syd/{interactive_viewer.py → viewer.py} +152 -188
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/METADATA +26 -12
- syd-0.1.7.dist-info/RECORD +22 -0
- syd/flask_deployment/static/css/styles.css +0 -39
- syd/flask_deployment/static/js/components.js +0 -51
- syd-0.1.6.dist-info/RECORD +0 -18
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/WHEEL +0 -0
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
class SydViewer {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.config = {
|
|
4
|
+
controlsPosition: 'left',
|
|
5
|
+
continuous: false,
|
|
6
|
+
updateInterval: 200,
|
|
7
|
+
...config
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
this.form = document.getElementById('controls-form');
|
|
11
|
+
this.plot = document.getElementById('plot');
|
|
12
|
+
this.updateTimer = null;
|
|
13
|
+
this.setupEventListeners();
|
|
14
|
+
this.updatePlot();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setupEventListeners() {
|
|
18
|
+
// Handle all form input changes
|
|
19
|
+
this.form.addEventListener('input', (event) => {
|
|
20
|
+
const input = event.target;
|
|
21
|
+
if (input.dataset.continuous === 'true' || this.config.continuous) {
|
|
22
|
+
this.debounceUpdate(() => this.handleInputChange(input));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Handle form changes for non-continuous updates
|
|
27
|
+
this.form.addEventListener('change', (event) => {
|
|
28
|
+
const input = event.target;
|
|
29
|
+
if (input.dataset.continuous !== 'true' && !this.config.continuous) {
|
|
30
|
+
this.handleInputChange(input);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Handle button clicks
|
|
35
|
+
this.form.querySelectorAll('button[type="button"]').forEach(button => {
|
|
36
|
+
button.addEventListener('click', () => this.handleButtonClick(button));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
debounceUpdate(callback) {
|
|
41
|
+
if (this.updateTimer) {
|
|
42
|
+
clearTimeout(this.updateTimer);
|
|
43
|
+
}
|
|
44
|
+
this.updateTimer = setTimeout(callback, this.config.updateInterval);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async handleInputChange(input) {
|
|
48
|
+
let value = this.getInputValue(input);
|
|
49
|
+
const name = input.name;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(`/update/${name}`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({ value }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await this.updatePlot();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Error updating parameter:', error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async handleButtonClick(button) {
|
|
71
|
+
const name = button.name;
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(`/update/${name}`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({ value: null }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await this.updatePlot();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Error handling button click:', error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getInputValue(input) {
|
|
92
|
+
switch (input.type) {
|
|
93
|
+
case 'checkbox':
|
|
94
|
+
return input.checked;
|
|
95
|
+
case 'number':
|
|
96
|
+
return parseFloat(input.value);
|
|
97
|
+
case 'range':
|
|
98
|
+
if (input.name.endsWith('_low')) {
|
|
99
|
+
const high = document.getElementById(input.id.replace('_low', '_high')).value;
|
|
100
|
+
return [parseFloat(input.value), parseFloat(high)];
|
|
101
|
+
} else if (input.name.endsWith('_high')) {
|
|
102
|
+
const low = document.getElementById(input.id.replace('_high', '_low')).value;
|
|
103
|
+
return [parseFloat(low), parseFloat(input.value)];
|
|
104
|
+
}
|
|
105
|
+
return parseFloat(input.value);
|
|
106
|
+
case 'select-multiple':
|
|
107
|
+
return Array.from(input.selectedOptions).map(option => {
|
|
108
|
+
const value = option.value;
|
|
109
|
+
return !isNaN(value) ? parseFloat(value) : value;
|
|
110
|
+
});
|
|
111
|
+
default:
|
|
112
|
+
const value = input.value;
|
|
113
|
+
return !isNaN(value) ? parseFloat(value) : value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async updatePlot() {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch('/plot');
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const data = await response.json();
|
|
125
|
+
if (data.error) {
|
|
126
|
+
throw new Error(data.error);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.plot.src = `data:image/png;base64,${data.image}`;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Error updating plot:', error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async updateState() {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch('/state');
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const state = await response.json();
|
|
143
|
+
this.syncFormWithState(state);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Error updating state:', error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
syncFormWithState(state) {
|
|
150
|
+
for (const [name, value] of Object.entries(state)) {
|
|
151
|
+
const input = this.form.elements[name];
|
|
152
|
+
if (!input) continue;
|
|
153
|
+
|
|
154
|
+
if (input.type === 'checkbox') {
|
|
155
|
+
input.checked = value;
|
|
156
|
+
} else if (input.type === 'select-multiple') {
|
|
157
|
+
Array.from(input.options).forEach(option => {
|
|
158
|
+
option.selected = value.includes(option.value);
|
|
159
|
+
});
|
|
160
|
+
} else if (input.type === 'range' && Array.isArray(value)) {
|
|
161
|
+
const [low, high] = value;
|
|
162
|
+
document.getElementById(`param_${name}_low`).value = low;
|
|
163
|
+
document.getElementById(`param_${name}_high`).value = high;
|
|
164
|
+
document.querySelector(`output[for="param_${name}_low"]`).value = low;
|
|
165
|
+
document.querySelector(`output[for="param_${name}_high"]`).value = high;
|
|
166
|
+
} else {
|
|
167
|
+
input.value = value;
|
|
168
|
+
if (input.type === 'range') {
|
|
169
|
+
document.querySelector(`output[for="${input.id}"]`).value = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
</style>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{% block title %}Syd Viewer{% endblock %}</title>
|
|
7
|
+
|
|
8
|
+
<!-- Bootstrap CSS -->
|
|
9
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
10
|
+
|
|
11
|
+
<!-- Custom CSS -->
|
|
12
|
+
<link href="{{ url_for('static', filename='css/viewer.css') }}" rel="stylesheet">
|
|
13
|
+
|
|
14
|
+
{% block extra_head %}{% endblock %}
|
|
16
15
|
</head>
|
|
17
16
|
<body>
|
|
18
|
-
|
|
17
|
+
<div class="container-fluid p-0">
|
|
18
|
+
{% block content %}{% endblock %}
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Bootstrap Bundle with Popper -->
|
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
23
|
+
|
|
24
|
+
<!-- Custom JavaScript -->
|
|
25
|
+
<script src="{{ url_for('static', filename='js/viewer.js') }}"></script>
|
|
19
26
|
|
|
20
|
-
{%
|
|
21
|
-
<script src="{{ js }}"></script>
|
|
22
|
-
{% endfor %}
|
|
23
|
-
<script src="{{ url_for('static', filename='js/components.js') }}"></script>
|
|
24
|
-
{% block scripts %}{% endblock %}
|
|
27
|
+
{% block extra_scripts %}{% endblock %}
|
|
25
28
|
</body>
|
|
26
|
-
</html>
|
|
29
|
+
</html>
|
|
@@ -1,97 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{% endif %}
|
|
23
|
-
}
|
|
24
|
-
#plot {
|
|
25
|
-
width: 100%;
|
|
26
|
-
height: auto;
|
|
27
|
-
}
|
|
28
|
-
</style>
|
|
29
|
-
</head>
|
|
30
|
-
<body>
|
|
31
|
-
<div class="container-fluid">
|
|
32
|
-
<div class="row">
|
|
33
|
-
<div class="controls">
|
|
34
|
-
{{ components_html | safe }}
|
|
35
|
-
</div>
|
|
36
|
-
<div class="plot-container">
|
|
37
|
-
<img id="plot" src="{{ initial_plot }}">
|
|
38
|
-
</div>
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block extra_head %}
|
|
4
|
+
<style>
|
|
5
|
+
:root {
|
|
6
|
+
--controls-width: {{ controls_width }}%;
|
|
7
|
+
--figure-width: {{ figure_width }}px;
|
|
8
|
+
--figure-height: {{ figure_height }}px;
|
|
9
|
+
}
|
|
10
|
+
</style>
|
|
11
|
+
{% endblock %}
|
|
12
|
+
|
|
13
|
+
{% block content %}
|
|
14
|
+
<div class="viewer-container {% if controls_position in ['left', 'right'] %}d-flex flex-row{% else %}d-flex flex-column{% endif %} h-100"
|
|
15
|
+
data-controls-position="{{ controls_position }}"
|
|
16
|
+
data-continuous="{{ continuous|default(false)|tojson }}">
|
|
17
|
+
{% if controls_position in ['left', 'top'] %}
|
|
18
|
+
<div class="controls-container {% if controls_position == 'left' %}w-controls{% else %}w-100{% endif %}">
|
|
19
|
+
<form id="controls-form" class="p-3">
|
|
20
|
+
{{ components|join('')|safe }}
|
|
21
|
+
</form>
|
|
39
22
|
</div>
|
|
23
|
+
{% endif %}
|
|
24
|
+
|
|
25
|
+
<div class="plot-container {% if controls_position in ['left', 'right'] %}flex-grow-1{% else %}w-100{% endif %} d-flex justify-content-center align-items-center p-3">
|
|
26
|
+
<img id="plot" src="{% if initial_plot %}data:image/png;base64,{{ initial_plot }}{% endif %}" alt="Plot" class="img-fluid">
|
|
40
27
|
</div>
|
|
41
|
-
|
|
42
|
-
{%
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
eval(js);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function buttonClick(name) {
|
|
71
|
-
fetch('/button_click', {
|
|
72
|
-
method: 'POST',
|
|
73
|
-
headers: {
|
|
74
|
-
'Content-Type': 'application/json',
|
|
75
|
-
},
|
|
76
|
-
body: JSON.stringify({name})
|
|
77
|
-
})
|
|
78
|
-
.then(response => response.json())
|
|
79
|
-
.then(data => {
|
|
80
|
-
if (data.error) {
|
|
81
|
-
console.error(data.error);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// Update plot
|
|
85
|
-
document.getElementById('plot').src = data.plot;
|
|
86
|
-
// Apply any parameter updates
|
|
87
|
-
for (const [param, js] of Object.entries(data.updates)) {
|
|
88
|
-
eval(js);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Initialize components
|
|
94
|
-
{{ components_init | safe }}
|
|
95
|
-
</script>
|
|
96
|
-
</body>
|
|
97
|
-
</html>
|
|
28
|
+
|
|
29
|
+
{% if controls_position in ['right', 'bottom'] %}
|
|
30
|
+
<div class="controls-container {% if controls_position == 'right' %}w-controls{% else %}w-100{% endif %}">
|
|
31
|
+
<form id="controls-form" class="p-3">
|
|
32
|
+
{{ components|join('')|safe }}
|
|
33
|
+
</form>
|
|
34
|
+
</div>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</div>
|
|
37
|
+
{% endblock %}
|
|
38
|
+
|
|
39
|
+
{% block extra_scripts %}
|
|
40
|
+
<script>
|
|
41
|
+
// Initialize the viewer with configuration
|
|
42
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
43
|
+
const container = document.querySelector('.viewer-container');
|
|
44
|
+
window.viewer = new SydViewer({
|
|
45
|
+
controlsPosition: container.dataset.controlsPosition,
|
|
46
|
+
continuous: JSON.parse(container.dataset.continuous),
|
|
47
|
+
updateInterval: 200
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
{% endblock %}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Testing Principles for SYD Flask Deployment
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Testing an interactive GUI application like the SYD Flask deployment presents unique challenges. While traditional test suites focus on programmatic API testing, interactive GUIs require a different approach that combines several testing methodologies. This document outlines a comprehensive testing strategy for the SYD Flask deployment.
|
|
6
|
+
|
|
7
|
+
## Testing Pyramid for Interactive GUIs
|
|
8
|
+
|
|
9
|
+
For the SYD Flask deployment, we recommend implementing a modified testing pyramid with the following layers:
|
|
10
|
+
|
|
11
|
+
1. **Unit Tests** - Test isolated components and functions
|
|
12
|
+
2. **Integration Tests** - Test interactions between components
|
|
13
|
+
3. **API Tests** - Test Flask API endpoints
|
|
14
|
+
4. **Headless Browser Tests** - Test GUI interactions programmatically
|
|
15
|
+
5. **Visual Regression Tests** - Ensure UI appearance remains consistent
|
|
16
|
+
6. **Manual Testing Checklists** - Structured human verification
|
|
17
|
+
|
|
18
|
+
## 1. Unit Tests
|
|
19
|
+
|
|
20
|
+
### What to Test
|
|
21
|
+
- Individual Flask routes
|
|
22
|
+
- Component HTML generation functions
|
|
23
|
+
- Parameter update handlers
|
|
24
|
+
- State synchronization logic
|
|
25
|
+
- Plot generation utilities
|
|
26
|
+
|
|
27
|
+
### Implementation Strategy
|
|
28
|
+
```python
|
|
29
|
+
import unittest
|
|
30
|
+
from unittest import mock
|
|
31
|
+
from syd.flask_deployment.deployer import FlaskDeployer
|
|
32
|
+
from syd.flask_deployment.components import create_component
|
|
33
|
+
|
|
34
|
+
class TestFlaskDeployerComponents(unittest.TestCase):
|
|
35
|
+
def setUp(self):
|
|
36
|
+
self.mock_viewer = mock.MagicMock()
|
|
37
|
+
self.deployer = FlaskDeployer(self.mock_viewer)
|
|
38
|
+
|
|
39
|
+
def test_create_float_component(self):
|
|
40
|
+
mock_param = mock.MagicMock()
|
|
41
|
+
mock_param.name = "test_param"
|
|
42
|
+
mock_param.value = 5.0
|
|
43
|
+
component = create_component(mock_param)
|
|
44
|
+
self.assertIn("test_param", component.html)
|
|
45
|
+
self.assertIn("5.0", component.html)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 2. Integration Tests
|
|
49
|
+
|
|
50
|
+
### What to Test
|
|
51
|
+
- Parameter creation to HTML rendering pipeline
|
|
52
|
+
- Parameter update → state sync → component update flow
|
|
53
|
+
- Plot generation and serving workflow
|
|
54
|
+
|
|
55
|
+
### Implementation Strategy
|
|
56
|
+
```python
|
|
57
|
+
def test_parameter_update_state_sync():
|
|
58
|
+
# Create real viewer with test parameters
|
|
59
|
+
viewer = Viewer()
|
|
60
|
+
viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
|
|
61
|
+
|
|
62
|
+
# Create deployer with this viewer
|
|
63
|
+
deployer = FlaskDeployer(viewer)
|
|
64
|
+
|
|
65
|
+
# Update parameter and check if components reflect the change
|
|
66
|
+
deployer._handle_parameter_update('test_param', 5.0)
|
|
67
|
+
|
|
68
|
+
# Verify component has updated
|
|
69
|
+
component = deployer.parameter_components['test_param']
|
|
70
|
+
self.assertEqual(component.value, 5.0)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 3. API Tests
|
|
74
|
+
|
|
75
|
+
### What to Test
|
|
76
|
+
- All Flask endpoints (`/`, `/update/<name>`, `/state`, `/plot`)
|
|
77
|
+
- Response formats and status codes
|
|
78
|
+
- Error handling
|
|
79
|
+
- Race conditions with concurrent requests
|
|
80
|
+
|
|
81
|
+
### Implementation Strategy
|
|
82
|
+
```python
|
|
83
|
+
def test_update_parameter_endpoint():
|
|
84
|
+
viewer = Viewer()
|
|
85
|
+
viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
|
|
86
|
+
deployer = FlaskDeployer(viewer)
|
|
87
|
+
app = deployer.app
|
|
88
|
+
|
|
89
|
+
with app.test_client() as client:
|
|
90
|
+
# Test successful update
|
|
91
|
+
response = client.post('/update/test_param',
|
|
92
|
+
json={'value': 5.0})
|
|
93
|
+
self.assertEqual(response.status_code, 200)
|
|
94
|
+
self.assertEqual(viewer.state['test_param'], 5.0)
|
|
95
|
+
|
|
96
|
+
# Test parameter not found
|
|
97
|
+
response = client.post('/update/nonexistent',
|
|
98
|
+
json={'value': 5.0})
|
|
99
|
+
self.assertEqual(response.status_code, 404)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 4. Headless Browser Tests
|
|
103
|
+
|
|
104
|
+
### What to Test
|
|
105
|
+
- User interactions with the web interface
|
|
106
|
+
- Parameter widget interactions
|
|
107
|
+
- UI updates in response to parameter changes
|
|
108
|
+
- Plot refreshes
|
|
109
|
+
|
|
110
|
+
### Implementation Strategy
|
|
111
|
+
|
|
112
|
+
Use Selenium or Playwright to automate browser interactions:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from selenium import webdriver
|
|
116
|
+
from selenium.webdriver.common.by import By
|
|
117
|
+
|
|
118
|
+
def test_slider_interaction():
|
|
119
|
+
# Start the Flask app in a separate thread
|
|
120
|
+
viewer = create_test_viewer()
|
|
121
|
+
deployer = FlaskDeployer(viewer)
|
|
122
|
+
thread = threading.Thread(target=deployer.app.run)
|
|
123
|
+
thread.daemon = True
|
|
124
|
+
thread.start()
|
|
125
|
+
|
|
126
|
+
# Use Selenium to interact with the web interface
|
|
127
|
+
driver = webdriver.Chrome()
|
|
128
|
+
driver.get('http://localhost:5000')
|
|
129
|
+
|
|
130
|
+
# Find a slider and interact with it
|
|
131
|
+
slider = driver.find_element(By.ID, 'param_amplitude')
|
|
132
|
+
slider.send_keys(webdriver.Keys.ARROW_RIGHT * 5) # Increase value
|
|
133
|
+
|
|
134
|
+
# Verify the plot updates
|
|
135
|
+
# This requires some way to detect that the plot has changed
|
|
136
|
+
# Could check the src attribute of the image element changes
|
|
137
|
+
time.sleep(1) # Allow time for update
|
|
138
|
+
plot_src = driver.find_element(By.ID, 'plot').get_attribute('src')
|
|
139
|
+
self.assertTrue(len(plot_src) > 0)
|
|
140
|
+
|
|
141
|
+
driver.quit()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 5. Visual Regression Tests
|
|
145
|
+
|
|
146
|
+
### What to Test
|
|
147
|
+
- UI appearance remains consistent across changes
|
|
148
|
+
- Components render correctly with different values
|
|
149
|
+
- Layout adapts properly to different screen sizes
|
|
150
|
+
|
|
151
|
+
### Implementation Strategy
|
|
152
|
+
|
|
153
|
+
Use tools like Percy or BackstopJS to capture and compare screenshots:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
def test_visual_appearance():
|
|
157
|
+
# Set up test app with standard parameters
|
|
158
|
+
app = create_standard_test_app()
|
|
159
|
+
|
|
160
|
+
# Use a tool like Percy
|
|
161
|
+
percy_snapshot(driver, 'Main view')
|
|
162
|
+
|
|
163
|
+
# Change to different layout
|
|
164
|
+
driver.get('http://localhost:5000?controls_position=right')
|
|
165
|
+
percy_snapshot(driver, 'Right controls layout')
|
|
166
|
+
|
|
167
|
+
# Test responsive views
|
|
168
|
+
driver.set_window_size(400, 800) # Mobile size
|
|
169
|
+
percy_snapshot(driver, 'Mobile view')
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 6. Manual Testing Checklists
|
|
173
|
+
|
|
174
|
+
For some aspects of interactive GUI applications, manual testing remains necessary. Create structured checklists for testers:
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
# Manual Testing Checklist
|
|
178
|
+
|
|
179
|
+
## Parameter Interactions
|
|
180
|
+
- [ ] Sliders respond smoothly to mouse drag
|
|
181
|
+
- [ ] Number inputs accept keyboard input
|
|
182
|
+
- [ ] Range sliders correctly display and update both values
|
|
183
|
+
- [ ] Toggles switch state when clicked
|
|
184
|
+
- [ ] Dropdowns show all options and select correctly
|
|
185
|
+
|
|
186
|
+
## Plot Updates
|
|
187
|
+
- [ ] Plot updates immediately after parameter change when continuous=True
|
|
188
|
+
- [ ] Plot updates only after interaction ends when continuous=False
|
|
189
|
+
- [ ] Plot maintains aspect ratio when window is resized
|
|
190
|
+
- [ ] No visual glitches during plot transitions
|
|
191
|
+
|
|
192
|
+
## Layouts
|
|
193
|
+
- [ ] Test all layouts: left, right, top, bottom
|
|
194
|
+
- [ ] Verify responsive behavior on different screen sizes
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Automated Testing Framework
|
|
198
|
+
|
|
199
|
+
To bring this all together, we recommend implementing a pytest-based testing framework:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# conftest.py
|
|
203
|
+
import pytest
|
|
204
|
+
from syd import make_viewer
|
|
205
|
+
import numpy as np
|
|
206
|
+
import matplotlib.pyplot as plt
|
|
207
|
+
|
|
208
|
+
@pytest.fixture
|
|
209
|
+
def standard_test_viewer():
|
|
210
|
+
"""Create a standard viewer for testing."""
|
|
211
|
+
viewer = make_viewer()
|
|
212
|
+
|
|
213
|
+
def plot(state):
|
|
214
|
+
fig, ax = plt.subplots()
|
|
215
|
+
x = np.linspace(0, 10, 1000)
|
|
216
|
+
y = state['amplitude'] * np.sin(state['frequency'] * x)
|
|
217
|
+
ax.plot(x, y)
|
|
218
|
+
return fig
|
|
219
|
+
|
|
220
|
+
viewer.set_plot(plot)
|
|
221
|
+
viewer.add_float('amplitude', value=1.0, min_value=0, max_value=2)
|
|
222
|
+
viewer.add_float('frequency', value=1.0, min_value=0.1, max_value=5)
|
|
223
|
+
|
|
224
|
+
return viewer
|
|
225
|
+
|
|
226
|
+
@pytest.fixture
|
|
227
|
+
def flask_app(standard_test_viewer):
|
|
228
|
+
"""Create a Flask app for testing."""
|
|
229
|
+
from syd.flask_deployment.deployer import FlaskDeployer
|
|
230
|
+
deployer = FlaskDeployer(standard_test_viewer)
|
|
231
|
+
return deployer.app
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Continuous Integration Setup
|
|
235
|
+
|
|
236
|
+
Integrate these tests into a CI pipeline:
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
# .github/workflows/flask-tests.yml
|
|
240
|
+
name: Flask Deployment Tests
|
|
241
|
+
|
|
242
|
+
on:
|
|
243
|
+
push:
|
|
244
|
+
paths:
|
|
245
|
+
- 'syd/flask_deployment/**'
|
|
246
|
+
- 'tests/flask/**'
|
|
247
|
+
|
|
248
|
+
jobs:
|
|
249
|
+
test:
|
|
250
|
+
runs-on: ubuntu-latest
|
|
251
|
+
steps:
|
|
252
|
+
- uses: actions/checkout@v2
|
|
253
|
+
- name: Set up Python
|
|
254
|
+
uses: actions/setup-python@v2
|
|
255
|
+
with:
|
|
256
|
+
python-version: '3.9'
|
|
257
|
+
- name: Install dependencies
|
|
258
|
+
run: |
|
|
259
|
+
python -m pip install --upgrade pip
|
|
260
|
+
pip install -e ".[test]"
|
|
261
|
+
- name: Run unit and integration tests
|
|
262
|
+
run: pytest tests/flask/test_unit_integration.py
|
|
263
|
+
- name: Run API tests
|
|
264
|
+
run: pytest tests/flask/test_api.py
|
|
265
|
+
- name: Set up Chrome WebDriver
|
|
266
|
+
uses: browser-actions/setup-chrome@latest
|
|
267
|
+
- name: Run headless browser tests
|
|
268
|
+
run: pytest tests/flask/test_browser.py
|
|
269
|
+
- name: Upload visual test artifacts
|
|
270
|
+
uses: actions/upload-artifact@v2
|
|
271
|
+
with:
|
|
272
|
+
name: visual-test-results
|
|
273
|
+
path: tests/flask/visual/results
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Practical Implementation Plan
|
|
277
|
+
|
|
278
|
+
For the SYD Flask deployment, we recommend implementing tests in this order:
|
|
279
|
+
|
|
280
|
+
1. **Start with Unit Tests** - Focus on individual components and functions
|
|
281
|
+
2. **Add API Tests** - Ensure all endpoints work correctly
|
|
282
|
+
3. **Implement Integration Tests** - Test component interactions
|
|
283
|
+
4. **Add Visual Testing** - Simple screenshot comparisons
|
|
284
|
+
5. **Create Browser Tests** - For critical UI workflows
|
|
285
|
+
|
|
286
|
+
## Testing Challenges and Solutions
|
|
287
|
+
|
|
288
|
+
| Challenge | Solution |
|
|
289
|
+
|-----------|----------|
|
|
290
|
+
| Testing real-time updates | Use JavaScript triggers and MutationObserver in browser tests |
|
|
291
|
+
| Asynchronous plot generation | Implement wait utilities with timeouts |
|
|
292
|
+
| Visual inconsistencies across platforms | Use Percy or similar to handle cross-platform rendering |
|
|
293
|
+
| Slow browser tests | Run only critical UI tests in CI, more comprehensive ones nightly |
|
|
294
|
+
| Matplotlib backend differences | Mock or standardize backends during testing |
|
|
295
|
+
|
|
296
|
+
## Conclusion
|
|
297
|
+
|
|
298
|
+
Testing an interactive GUI like the SYD Flask deployment requires a multi-layered approach. By combining traditional unit and integration tests with specialized GUI testing tools, we can achieve good test coverage while still accommodating the unique challenges of interactive web applications. The most effective strategy is to automate as much as possible while maintaining a structured approach to manual testing for aspects that are difficult to verify programmatically.
|
|
299
|
+
|
|
300
|
+
For the initial implementation of the testing framework, focus on establishing solid unit and API tests, as these will provide the foundation for more complex tests and help catch the most common issues early in the development process.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
from .deployer import
|
|
1
|
+
from .deployer import NotebookDeployer
|