forked from fixin.me/fixin.me
Compare commits
1 Commits
4f10a4fcf8
...
feature/me
| Author | SHA1 | Date | |
|---|---|---|---|
| 887d669f80 |
22
CLAUDE.md
22
CLAUDE.md
@@ -77,28 +77,6 @@ default/ namespace for default units import/export and admin panel
|
|||||||
root → /units (authenticated), /sign_in (unauthenticated)
|
root → /units (authenticated), /sign_in (unauthenticated)
|
||||||
```
|
```
|
||||||
|
|
||||||
## JavaScript Conventions
|
|
||||||
|
|
||||||
### No manual fetch() — use Turbo
|
|
||||||
|
|
||||||
Never make AJAX requests with `fetch()` in JavaScript. Use Turbo's built-in mechanisms instead:
|
|
||||||
|
|
||||||
- **Links/buttons that trigger server actions**: use `data: {turbo_stream: true}` on the element (link or button_to form).
|
|
||||||
- **Dynamic form submissions from JS** (where HTML alone isn't enough): create a form element, append hidden inputs, and call `form.requestSubmit()`. Turbo intercepts it automatically — no manual CSRF handling, no `Turbo.renderStreamMessage()`.
|
|
||||||
```javascript
|
|
||||||
var form = document.createElement('form');
|
|
||||||
form.action = url; form.method = 'post'; form.dataset.turboStream = 'true';
|
|
||||||
// append hidden inputs...
|
|
||||||
form.addEventListener('turbo:submit-end', function() { form.remove(); });
|
|
||||||
document.body.appendChild(form);
|
|
||||||
form.requestSubmit();
|
|
||||||
```
|
|
||||||
- **Server-rendered HTML**: use ERB partials and Turbo Stream views (`*.turbo_stream.erb`), never build HTML in JavaScript.
|
|
||||||
|
|
||||||
### No HTML generation in JavaScript
|
|
||||||
|
|
||||||
Never use JavaScript to build and insert HTML (no `innerHTML =`, no `createElement` trees for content). Render HTML server-side in ERB partials; update the DOM via Turbo Stream actions (`replace`, `update`, `append`, etc.).
|
|
||||||
|
|
||||||
## Database Requirements
|
## Database Requirements
|
||||||
|
|
||||||
The database must support:
|
The database must support:
|
||||||
|
|||||||
@@ -111,11 +111,6 @@ svg {
|
|||||||
svg:last-child {
|
svg:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
.chart-panel svg {
|
|
||||||
height: auto;
|
|
||||||
margin: 0;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
textarea {
|
textarea {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -671,9 +666,6 @@ body[data-measurements-view=wide] .view-toggle[data-view=wide] {
|
|||||||
color: white;
|
color: white;
|
||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
.chart-panel {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#measurements tr.grouped td {
|
#measurements tr.grouped td {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
class ChartsController < ApplicationController
|
|
||||||
def index
|
|
||||||
readouts = current_user.readouts.includes(:quantity, :unit).order(:taken_at, :id)
|
|
||||||
@readouts_json = readouts.map { |r|
|
|
||||||
{ takenAt: r.taken_at&.iso8601,
|
|
||||||
quantityId: r.quantity_id,
|
|
||||||
quantityName: r.quantity.name,
|
|
||||||
value: r.value.to_f,
|
|
||||||
unit: r.unit.symbol }
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -171,7 +171,6 @@ module ApplicationHelper
|
|||||||
['measurements', 'scale-bathroom', :restricted],
|
['measurements', 'scale-bathroom', :restricted],
|
||||||
['quantities', 'axis-arrow', :restricted, 'right'],
|
['quantities', 'axis-arrow', :restricted, 'right'],
|
||||||
['units', 'weight-gram', :restricted],
|
['units', 'weight-gram', :restricted],
|
||||||
['charts', 'chart-line', :restricted],
|
|
||||||
# TODO: display users tab only if >1 user present; sole_user?/sole_admin?
|
# TODO: display users tab only if >1 user present; sole_user?/sole_admin?
|
||||||
['users', 'account-multiple-outline', :admin],
|
['users', 'account-multiple-outline', :admin],
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,54 +23,6 @@ function groupMeasurements() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function buildCharts() {
|
|
||||||
var container = document.getElementById('measurements-charts');
|
|
||||||
var dataEl = document.getElementById('charts-data');
|
|
||||||
if (!container || !dataEl) return;
|
|
||||||
|
|
||||||
var readouts = JSON.parse(dataEl.textContent);
|
|
||||||
container.innerHTML = '';
|
|
||||||
if (readouts.length === 0) return;
|
|
||||||
|
|
||||||
// Data arrives sorted by taken_at from the server; group into per-quantity traces
|
|
||||||
var quantities = new Map();
|
|
||||||
readouts.forEach(function(r) {
|
|
||||||
if (!r.takenAt) return;
|
|
||||||
if (!quantities.has(r.quantityId)) {
|
|
||||||
quantities.set(r.quantityId, { name: r.quantityName, unit: r.unit, x: [], y: [] });
|
|
||||||
}
|
|
||||||
var q = quantities.get(r.quantityId);
|
|
||||||
q.x.push(r.takenAt);
|
|
||||||
q.y.push(r.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
var traces = [];
|
|
||||||
quantities.forEach(function(q) {
|
|
||||||
traces.push({
|
|
||||||
x: q.x,
|
|
||||||
y: q.y,
|
|
||||||
mode: 'lines+markers',
|
|
||||||
type: 'scatter',
|
|
||||||
name: q.name + ' (' + q.unit + ')',
|
|
||||||
marker: { size: 5 }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var div = document.createElement('div');
|
|
||||||
div.className = 'chart-panel';
|
|
||||||
container.appendChild(div);
|
|
||||||
|
|
||||||
Plotly.newPlot(div, traces, {
|
|
||||||
xaxis: { type: 'date', tickformat: '%Y-%m-%d %H:%M' },
|
|
||||||
yaxis: {},
|
|
||||||
margin: { t: 20, r: 20, b: 80, l: 60 },
|
|
||||||
paper_bgcolor: 'transparent',
|
|
||||||
plot_bgcolor: 'transparent',
|
|
||||||
font: { family: 'system-ui' },
|
|
||||||
legend: { orientation: 'h', y: -0.25 }
|
|
||||||
}, { responsive: true, displayModeBar: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMeasurementsView() {
|
function getMeasurementsView() {
|
||||||
return localStorage.getItem('measurements-view') || 'compact';
|
return localStorage.getItem('measurements-view') || 'compact';
|
||||||
}
|
}
|
||||||
@@ -86,10 +38,6 @@ function setMeasurementsView(view) {
|
|||||||
window.setMeasurementsView = setMeasurementsView
|
window.setMeasurementsView = setMeasurementsView
|
||||||
|
|
||||||
document.addEventListener('turbo:load', function() {
|
document.addEventListener('turbo:load', function() {
|
||||||
if (document.getElementById('charts-data')) {
|
|
||||||
buildCharts();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var tbody = document.getElementById('measurements');
|
var tbody = document.getElementById('measurements');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
groupMeasurements();
|
groupMeasurements();
|
||||||
@@ -134,13 +82,9 @@ window.detailsObserver = new MutationObserver((mutations) => {
|
|||||||
function readoutUnitChanged(select) {
|
function readoutUnitChanged(select) {
|
||||||
var button = select.closest('tr').querySelector('.set-default-unit');
|
var button = select.closest('tr').querySelector('.set-default-unit');
|
||||||
if (select.value && select.value !== select.dataset.defaultUnitId) {
|
if (select.value && select.value !== select.dataset.defaultUnitId) {
|
||||||
button.removeAttribute('disabled');
|
Turbo.StreamElement.prototype.enableElement(button);
|
||||||
button.removeAttribute('aria-disabled');
|
|
||||||
button.removeAttribute('tabindex');
|
|
||||||
} else {
|
} else {
|
||||||
button.setAttribute('disabled', 'disabled');
|
Turbo.StreamElement.prototype.disableElement(button);
|
||||||
button.setAttribute('aria-disabled', 'true');
|
|
||||||
button.setAttribute('tabindex', '-1');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.readoutUnitChanged = readoutUnitChanged
|
window.readoutUnitChanged = readoutUnitChanged
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
<div class="main-area" id="measurements-charts"></div>
|
|
||||||
<script id="charts-data" type="application/json"><%= raw @readouts_json %></script>
|
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
<%= stylesheet_link_tag "spreadsheet" %>
|
<%= stylesheet_link_tag "spreadsheet" %>
|
||||||
<script src="https://cdn.plot.ly/plotly-basic-2.35.2.min.js"></script>
|
|
||||||
<%= javascript_importmap_tags %>
|
<%= javascript_importmap_tags %>
|
||||||
|
|
||||||
<%#= turbo_page_requires_reload_tag %>
|
<%#= turbo_page_requires_reload_tag %>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
<%= image_button_tag '', 'view-columns', name: nil, type: 'button',
|
<%= image_button_tag '', 'view-columns', name: nil, type: 'button',
|
||||||
class: 'view-toggle', title: t('.view_wide'),
|
class: 'view-toggle', title: t('.view_wide'),
|
||||||
data: {view: 'wide'}, onclick: "setMeasurementsView('wide')" %>
|
data: {view: 'wide'}, onclick: "setMeasurementsView('wide')" %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-area measurements-section">
|
<div class="main-area measurements-section">
|
||||||
|
|||||||
@@ -89,8 +89,6 @@ en:
|
|||||||
readouts:
|
readouts:
|
||||||
form:
|
form:
|
||||||
set_default_unit: Set as default unit
|
set_default_unit: Set as default unit
|
||||||
charts:
|
|
||||||
navigation: Charts
|
|
||||||
measurements:
|
measurements:
|
||||||
navigation: Measurements
|
navigation: Measurements
|
||||||
no_items: There are no measurements taken. You can Add some now.
|
no_items: There are no measurements taken. You can Add some now.
|
||||||
@@ -101,7 +99,6 @@ en:
|
|||||||
new_measurement: Add measurement
|
new_measurement: Add measurement
|
||||||
view_compact: Compact view
|
view_compact: Compact view
|
||||||
view_wide: Wide view
|
view_wide: Wide view
|
||||||
view_charts: Charts
|
|
||||||
readout:
|
readout:
|
||||||
edit: Edit
|
edit: Edit
|
||||||
destroy: Delete
|
destroy: Delete
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
resources :measurements
|
resources :measurements
|
||||||
resources :charts, only: [:index]
|
|
||||||
|
|
||||||
resources :readouts, only: [:new] do
|
resources :readouts, only: [:new] do
|
||||||
collection {get 'new/:id/discard', action: :discard, as: :discard}
|
collection {get 'new/:id/discard', action: :discard, as: :discard}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
class AddTakenAtToReadouts < ActiveRecord::Migration[7.2]
|
class AddTakenAtToReadouts < ActiveRecord::Migration[7.2]
|
||||||
def change
|
def change
|
||||||
add_column :readouts, :taken_at, :datetime
|
add_column :readouts, :taken_at, :datetime
|
||||||
add_index :readouts, [:user_id, :taken_at]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ ActiveRecord::Schema[7.2].define(version: 2026_04_03_000000) do
|
|||||||
t.index ["quantity_id"], name: "index_readouts_on_quantity_id"
|
t.index ["quantity_id"], name: "index_readouts_on_quantity_id"
|
||||||
t.index ["unit_id"], name: "index_readouts_on_unit_id"
|
t.index ["unit_id"], name: "index_readouts_on_unit_id"
|
||||||
t.index ["user_id"], name: "index_readouts_on_user_id"
|
t.index ["user_id"], name: "index_readouts_on_user_id"
|
||||||
t.index ["user_id", "taken_at"], name: "index_readouts_on_user_id_and_taken_at"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "units", charset: "utf8mb4", collation: "utf8mb4_0900_as_ci", force: :cascade do |t|
|
create_table "units", charset: "utf8mb4", collation: "utf8mb4_0900_as_ci", force: :cascade do |t|
|
||||||
|
|||||||
@@ -21,4 +21,3 @@ end
|
|||||||
#[Source, Quantity, Unit].each { |model| model.defaults.delete_all }
|
#[Source, Quantity, Unit].each { |model| model.defaults.delete_all }
|
||||||
|
|
||||||
require_relative 'seeds/units.rb'
|
require_relative 'seeds/units.rb'
|
||||||
require_relative 'seeds/demo.rb'
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
demo_email = 'demo@localhost'
|
|
||||||
|
|
||||||
User.transaction do
|
|
||||||
break if User.find_by(email: demo_email)
|
|
||||||
|
|
||||||
demo = User.create! email: demo_email, password: 'demo123', status: :active do |user|
|
|
||||||
user.skip_confirmation!
|
|
||||||
print "Creating demo account '#{user.email}' with password '#{user.password}'..."
|
|
||||||
end
|
|
||||||
puts "done."
|
|
||||||
|
|
||||||
# --- Units ---
|
|
||||||
u = {}
|
|
||||||
u[:kg] = demo.units.create! symbol: 'kg', description: 'kilogram'
|
|
||||||
u[:g] = demo.units.create! symbol: 'g', description: 'gram', base: u[:kg], multiplier: 1e-3
|
|
||||||
u[:cm] = demo.units.create! symbol: 'cm', description: 'centimetre'
|
|
||||||
u[:bpm] = demo.units.create! symbol: 'bpm', description: 'beats per minute'
|
|
||||||
u[:pct] = demo.units.create! symbol: '%', description: 'percent'
|
|
||||||
u[:h] = demo.units.create! symbol: 'h', description: 'hour'
|
|
||||||
u[:min] = demo.units.create! symbol: 'min', description: 'minute', base: u[:h], multiplier: (1.0/60).round(10)
|
|
||||||
u[:kcal]= demo.units.create! symbol: 'kcal',description: 'kilocalorie'
|
|
||||||
u[:mg] = demo.units.create! symbol: 'mg', description: 'milligram', base: u[:kg], multiplier: 1e-6
|
|
||||||
u[:mmhg]= demo.units.create! symbol: 'mmHg',description: 'millimetre of mercury'
|
|
||||||
|
|
||||||
# --- Quantities ---
|
|
||||||
body = demo.quantities.create! name: 'Body'
|
|
||||||
weight = demo.quantities.create! name: 'Weight', parent: body
|
|
||||||
height = demo.quantities.create! name: 'Height', parent: body
|
|
||||||
fat = demo.quantities.create! name: 'Body fat', parent: body
|
|
||||||
|
|
||||||
cardio = demo.quantities.create! name: 'Cardiovascular'
|
|
||||||
hr_rest = demo.quantities.create! name: 'Resting HR', parent: cardio
|
|
||||||
hr_peak = demo.quantities.create! name: 'Peak HR', parent: cardio
|
|
||||||
bp_sys = demo.quantities.create! name: 'BP systolic', parent: cardio
|
|
||||||
bp_dia = demo.quantities.create! name: 'BP diastolic', parent: cardio
|
|
||||||
|
|
||||||
activity = demo.quantities.create! name: 'Activity'
|
|
||||||
sleep_dur = demo.quantities.create! name: 'Sleep', parent: activity
|
|
||||||
calories = demo.quantities.create! name: 'Calories out', parent: activity
|
|
||||||
|
|
||||||
nutrition = demo.quantities.create! name: 'Nutrition'
|
|
||||||
cal_in = demo.quantities.create! name: 'Calories in', parent: nutrition
|
|
||||||
caffeine = demo.quantities.create! name: 'Caffeine', parent: nutrition
|
|
||||||
|
|
||||||
# --- Readouts (60 days of daily-ish data) ---
|
|
||||||
base_time = 60.days.ago.beginning_of_day
|
|
||||||
rng = Random.new(42)
|
|
||||||
|
|
||||||
weight_val = 82.4
|
|
||||||
fat_val = 21.5
|
|
||||||
hr_rest_val = 62.0
|
|
||||||
|
|
||||||
60.times do |i|
|
|
||||||
t = base_time + i.days + rng.rand(3600 * 2)
|
|
||||||
|
|
||||||
weight_val += rng.rand(-0.3..0.3)
|
|
||||||
fat_val += rng.rand(-0.15..0.15)
|
|
||||||
hr_rest_val += rng.rand(-1.5..1.5)
|
|
||||||
hr_rest_val = hr_rest_val.clamp(52, 72)
|
|
||||||
|
|
||||||
demo.readouts.create! quantity: weight, unit: u[:kg], value: weight_val.round(1), created_at: t
|
|
||||||
demo.readouts.create! quantity: fat, unit: u[:pct], value: fat_val.round(1), created_at: t
|
|
||||||
demo.readouts.create! quantity: hr_rest, unit: u[:bpm], value: hr_rest_val.round, created_at: t
|
|
||||||
|
|
||||||
if i % 2 == 0
|
|
||||||
demo.readouts.create! quantity: bp_sys, unit: u[:mmhg], value: (115 + rng.rand(-8..8)).round, created_at: t
|
|
||||||
demo.readouts.create! quantity: bp_dia, unit: u[:mmhg], value: (75 + rng.rand(-5..5)).round, created_at: t
|
|
||||||
end
|
|
||||||
|
|
||||||
if i % 3 == 0
|
|
||||||
demo.readouts.create! quantity: hr_peak, unit: u[:bpm], value: (155 + rng.rand(-10..10)).round, created_at: t
|
|
||||||
end
|
|
||||||
|
|
||||||
demo.readouts.create! quantity: sleep_dur, unit: u[:h], value: (6.5 + rng.rand(-1.5..1.5)).round(1), created_at: t
|
|
||||||
demo.readouts.create! quantity: calories, unit: u[:kcal],value: (2100 + rng.rand(-300..300)).round, created_at: t
|
|
||||||
demo.readouts.create! quantity: cal_in, unit: u[:kcal],value: (1900 + rng.rand(-400..400)).round, created_at: t
|
|
||||||
|
|
||||||
if i % 4 == 0
|
|
||||||
demo.readouts.create! quantity: caffeine, unit: u[:mg], value: (200 + rng.rand(-80..80)).round, created_at: t
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# height is stable — record once
|
|
||||||
demo.readouts.create! quantity: height, unit: u[:cm], value: 178.0, created_at: base_time
|
|
||||||
|
|
||||||
puts " Created #{demo.units.count} units, #{demo.quantities.count} quantities, #{demo.readouts.count} readouts."
|
|
||||||
end
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class ChartsControllerTest < ActionDispatch::IntegrationTest
|
|
||||||
setup do
|
|
||||||
host! '127.0.0.1'
|
|
||||||
@user = users(:alice)
|
|
||||||
post new_user_session_path, params: { user: { email: @user.email, password: 'alice' } }
|
|
||||||
@quantity = @user.quantities.create!(name: 'Weight')
|
|
||||||
@unit = @user.units.create!(symbol: 'kg')
|
|
||||||
end
|
|
||||||
|
|
||||||
test "requires authentication" do
|
|
||||||
delete destroy_user_session_path
|
|
||||||
get charts_path
|
|
||||||
assert_response :redirect
|
|
||||||
end
|
|
||||||
|
|
||||||
test "index returns ok" do
|
|
||||||
get charts_path
|
|
||||||
assert_response :success
|
|
||||||
end
|
|
||||||
|
|
||||||
test "embeds readout data as JSON in script tag" do
|
|
||||||
users(:alice).readouts.create!(quantity: @quantity, unit: @unit, value: 82.5, taken_at: 1.day.ago)
|
|
||||||
|
|
||||||
get charts_path
|
|
||||||
|
|
||||||
assert_select 'script#charts-data[type="application/json"]' do |elements|
|
|
||||||
data = JSON.parse(elements.first.children.first.to_s)
|
|
||||||
assert_equal 1, data.size
|
|
||||||
assert_equal 'Weight', data.first['quantityName']
|
|
||||||
assert_in_delta 82.5, data.first['value']
|
|
||||||
assert_equal 'kg', data.first['unit']
|
|
||||||
assert_not_nil data.first['takenAt']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "orders readouts by taken_at ascending" do
|
|
||||||
older = users(:alice).readouts.create!(quantity: @quantity, unit: @unit, value: 80.0, taken_at: 2.days.ago)
|
|
||||||
newer = users(:alice).readouts.create!(quantity: @quantity, unit: @unit, value: 82.5, taken_at: 1.day.ago)
|
|
||||||
|
|
||||||
get charts_path
|
|
||||||
|
|
||||||
assert_select 'script#charts-data[type="application/json"]' do |elements|
|
|
||||||
data = JSON.parse(elements.first.children.first.to_s)
|
|
||||||
assert_equal older.taken_at.iso8601, data.first['takenAt']
|
|
||||||
assert_equal newer.taken_at.iso8601, data.last['takenAt']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not expose other users readouts" do
|
|
||||||
bob_quantity = users(:bob).quantities.create!(name: 'Steps')
|
|
||||||
bob_unit = users(:bob).units.create!(symbol: 'steps')
|
|
||||||
users(:bob).readouts.create!(quantity: bob_quantity, unit: bob_unit, value: 5000, taken_at: 1.day.ago)
|
|
||||||
|
|
||||||
get charts_path
|
|
||||||
|
|
||||||
assert_select 'script#charts-data[type="application/json"]' do |elements|
|
|
||||||
data = JSON.parse(elements.first.children.first.to_s)
|
|
||||||
assert data.none? { |r| r['quantityName'] == 'Steps' }, "Bob's data must not appear"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
require "application_system_test_case"
|
|
||||||
|
|
||||||
class ChartsTest < ApplicationSystemTestCase
|
|
||||||
setup do
|
|
||||||
@user = sign_in(user: users(:alice))
|
|
||||||
@quantity = @user.quantities.create!(name: 'Weight')
|
|
||||||
@unit = @user.units.create!(symbol: 'kg')
|
|
||||||
@user.readouts.create!(quantity: @quantity, unit: @unit, value: 82.5, taken_at: 1.day.ago)
|
|
||||||
@user.readouts.create!(quantity: @quantity, unit: @unit, value: 83.1, taken_at: Time.now)
|
|
||||||
visit charts_path
|
|
||||||
end
|
|
||||||
|
|
||||||
test "charts page is reachable from navigation" do
|
|
||||||
visit root_path
|
|
||||||
click_on t('charts.navigation')
|
|
||||||
assert_current_path charts_path
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders Plotly chart panel" do
|
|
||||||
assert_selector '#measurements-charts .chart-panel', wait: 5
|
|
||||||
end
|
|
||||||
|
|
||||||
test "chart legend shows quantity name with unit" do
|
|
||||||
assert_text 'Weight (kg)', wait: 5
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user