Repository navigation
Expand file tree
/
Copy pathtest-matrix.yml
More file actions
1118 lines (1044 loc) · 50.9 KB
/
Copy pathtest-matrix.yml
File metadata and controls
1118 lines (1044 loc) · 50.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# The cross-platform test matrix.
#
# One row per test that the app test suites implement. This file is the
# parity mechanism for testing, the way spec/*.yml is for the file format:
# Android and iOS must cover the same rows (minus recorded platform
# differences), and tools/check_test_matrix.py - run by the docs build when
# the app checkouts sit next to this repository - verifies it mechanically
# instead of by discipline.
#
# Each app test carries its row id in a tag comment in its source:
#
# // phyphox-test: corpus-valid-load (Kotlin/Java)
# // phyphox-test: corpus-valid-load (Swift)
#
# The checker greps both repos for these tags. A tag that names no row here
# fails the build (typo guard). A row's meaning by status:
#
# planned - agreed and listed, not yet implemented everywhere. Missing
# tags are reported as information, never as a failure.
# active - implemented on every platform the row names. From then on a
# missing tag FAILS the docs build: a deleted or renamed test
# is a regression, not a cleanup. Rows are flipped to active by
# the docs session once both platforms carry the tag.
#
# Fields: id (kebab-case, the tag), area (a short topic label), tier (T0-T3),
# platforms (subset of [android, ios]; name only one for recorded platform
# differences and say why in the description), status, description, and the
# optional host_driven: true for a row implemented entirely in this
# repository's tools/ - for those, and only those, a tag found there counts
# as the implementation for every platform the row names.
#
# Open question (2026-08-31, from the editor audit): the Blockly editor now
# also runs the corpus - its AUDIT=true vitest suite round-trips every
# corpus file against this repo's spec. Whether the editor becomes a third
# platform here (with the checker grepping phyphox-blockly-editor too, and
# per-row applicability, since a generator-and-parser covers different rows
# than a parser) is the maintainer's call; recorded so the idea is not lost.
tests:
- id: corpus-valid-load
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
Every file in corpus/valid and corpus/generated whose declared format
version is at most the platform's supported version loads without
error. Files declaring a newer version are skipped, not failed - they
exist for future format versions. A file with an entry in the
directory's expected.yml deviates per platform: the platform mapped
to rejects asserts the refusal instead (deliberate platform
differences, e.g. bluetooth address= on iOS). See corpus/README.md,
"The app test suites".
- id: unit-reference-parse
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
The unit attributes are parsed to logical units (spec/rules.yml,
unit-reference; spec/units.yml): in a 1.21 file `@meter` yields the
known unit meter, shown with the app's symbol; `[[unit_short_meter]]`
yields the same unit in any version; a literal `m`, an `@meter` in a
file declaring 1.20, and an unknown `@metre` in a 1.21 file are custom
text, shown verbatim and not convertible (UnitReferenceParseTest /
UnitReferenceParseTests). Specified 2026-09-30 ahead of the
implementations, active the same day.
- id: unit-conversion-display
area: ui
tier: T0
platforms: [android, ios]
status: active
description: >-
What the view elements DISPLAY follows the unit conversion
(docs/file-format/units.md, "Conversion in the app"), checked on the
corpus fixture generated/unit-references.phyphox without rendering:
under each of the three unit-system settings (experiment, metric,
imperial) and after a programmatic per-element switch (what the unit
dialog does), the value element's text and unit, the edit element's
shown value, unit and limits, the graph's axis titles, tic labels and
the data picker's point/difference/slope read-outs equal the expected
strings - including the precision rule (cm with one decimal shows two
in inches), the affine temperature (°C position vs. Δ), the composed
slope unit once an axis is switched, and that text units, a value
with positiveUnit/negativeUnit and an edit with decimal="false" are
untouched. Android: Robolectric against the elements and GraphView
(UnitConversionDisplayTest, GraphUnitConversionTest); iOS: XCTest
against the view descriptors and the graph module
(UnitConversionDisplayTests). Specified 2026-09-30 ahead of the
implementations, active the same day.
- id: unit-conversion-ui
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
The user-facing path of the unit conversion on the running app
(Espresso / XCUITest, on the emulator or simulator, with
generated/unit-references.phyphox loaded): tapping the unit text of a
value and of an edit element, and an axis label of a graph in its
exclusive mode, opens the unit dialog listing the units of that
quantity with the experiment's unit marked; choosing another unit
changes the displayed text immediately and the data itself does not
change (the buffer value read back through the remote interface or
the export is the original); the Unit system setting converts every
convertible element on the next experiment load and leaves the
exclusions alone (UnitConversionUiTest / UnitConversionUITests).
Specified 2026-09-30 ahead of the implementations, active the same
day.
- id: corpus-invalid-reject
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
Every corpus/invalid file whose expected.yml entry says
`parser: rejects` fails to load. Any error is acceptable; error
message texts are platform wording and are not asserted.
- id: corpus-tolerated-attributes-load
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
Every corpus/invalid file whose expected.yml entry says
`parser: accepts` LOADS successfully: its defects are tolerated for
compatibility per the unknown-attribute-ignored and
duplicate-metadata-last-wins rules (spec/rules.yml). This pins the
compatibility guarantee - a parser that starts rejecting unknown
attributes or legacy duplicates breaks files in the wild.
- id: analysis-golden-vectors
area: analysis
tier: T0
platforms: [android, ios]
status: active
description: >-
Every case under corpus/analysis/vectors/ (a mini-experiment plus an
.expected.json) loads through the real parser, its analysis block is
executed the stated number of cycles, and the listed output buffers
match the expected values - NaN-aware, within the stated tolerances.
Cases whose file declares a newer format version than the platform
supports are skipped, like corpus-valid-load. See
corpus/analysis/README.md for the runner contract.
- id: audio-output-retrigger
area: output
tier: T0
platforms: [android, ios]
status: active
description: >-
What the trigger at the end of an analysis cycle does to an audio
output that is still playing (spec/output.yml, loop): a looped output
plays on undisturbed, a one-shot output starts over from its beginning
- the direct source from its first sample, a tone's duration from zero
- whether or not the previous playback had finished, and a one-shot
output ends with the data it has now, so a source that grew plays on
and one that shrank stops earlier. Blocks are generated through
nextBlock() without an audio engine: Robolectric on Android
(AudioOutputRetriggerTest, which also drives the sample index past the
int range), XCTest on iOS (AudioOutputRetriggerTests). Added 2026-10-04
with the fix of the former audio-output-retrigger inconsistency.
Implemented and tested on both platforms 2026-10-04.
- id: corpus-version-gate
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
A file declaring a format version newer than the app supports (the
supported version + 0.1) is refused with the app's file-too-new
handling, and one declaring the exact supported version loads. Guards
the version gate that all feature rollout relies on.
- id: translation-block-selection
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
Which translation block a file gets (file-format/index.md, "Block:
translations"): a German block is applied on a German device and not on
an English, French or English-in-Germany one, with and without a root
locale, and also on a device locale without a region (as an in-app
language setting produces); with two blocks the better-matching one
wins. Written 2026-09-27 after a report that Android applied a German
block regardless of the device language when the root locale was
omitted; the decided rule (2026-09-27) treats a missing root locale as
"en", and a block carrying the root's own locale stands in for the
base strings unless another block rates strictly better (the bundled
light experiment has no base strings and relies on this); the row
asserts both.
- id: sensor-prefer-uncalibrated
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
The preferUncalibrated attribute of the sensor element (format 1.21)
decides which version of a sensor the experiment starts with:
corpus/generated/sensor-prefer-uncalibrated.phyphox asks for the
uncalibrated magnetometer and the default (calibrated) gyroscope, and
the loaded sensor inputs must carry exactly that.
- id: remote-contract-t1
area: remote-interface
tier: T1
platforms: [android, ios]
status: active
description: >-
tools/contract_test.py in single-target mode against the platform's
emulator/simulator build: every response validates against the
OpenAPI schemas and the annotated contract semantics (exact statuses
and result values). Host-driven - the phyphox-test tag lives in the
repo's T1 CI workflow, not in app source. Phone-vs-phone two-target
runs stay the release gate.
- id: webinterface-graphs-t0
area: remote-interface
tier: T0
platforms: [android, ios]
status: active
description: >-
The phyphox-webinterface browser suite (test/webinterface.test.js in the
submodule, node --test with puppeteer-core driving a headless Chromium)
against its mock server: one chart per graph, the embedded graph
configuration and translated strings, partial updates, mouse (drag pans,
shift+drag zooms a box, wheel) and touch (pinch, one-finger pan, tap
picks) interaction, follow with and without a configured window and
followX from the first frame, the data picker (nearest point on screen,
hover preview, two-point span) with its outputs written through /set,
log toggles, tap-to-maximize and the phone layout, color maps, the
view groups (weights, grid columns, stacks and transforms from a
bound buffer, maximize inside a group, fixed plot area, alpha colours), the
empty-plot hints, clock-labelled time axes, view switching, bright mode
and the legacy app-generated element functions. No phone involved.
Host-driven per repo: the tag lives in each app repo's T0 CI workflow,
which runs the submodule's suite. Added 2026-09-20, active 2026-09-21.
- id: webinterface-served-t1
area: remote-interface
tier: T1
platforms: [android, ios]
status: active
description: >-
The same webinterface suite (PHYPHOX_URL set) against the interface the
platform's emulator/simulator build actually serves, with
test/fixtures/webgraphs.phyphox open in the app and remote access
enabled through the shell-only switch. Verifies the app's side of the
view-layout contract: the graph JSON, the translated strings, the
picker writes into real buffers. Tests for graphs the fixture does not
contain are skipped. Host-driven per repo: the tag lives in the T1 CI
workflow. Added 2026-09-20, active 2026-09-21.
- id: network-http
area: network
tier: T1
platforms: [android, ios]
status: active
description: >-
The http fixture experiments (phyphox-docs fixtures/network/, served
by tools/network_fixture.py): receive sequence, GET and POST send
round trips, malformed-response and server-down error paths - no
crash, no hang, buffer contents asserted through the remote API.
See fixtures/network/README.md for the runner contract including
the FIXTURE-HOST substitution.
- id: network-mqtt
area: network
tier: T1
platforms: [android, ios]
status: active
description: >-
The mqtt fixture experiment against a local mosquitto broker
(fixtures/network/mosquitto.conf): publish/subscribe loopback
through a real broker; the TLS variants join once the mqtts
fixtures exist.
- id: experiments-e2e-t1
area: experiments
tier: T1
platforms: [android, ios]
status: active
description: >-
tools/t1_experiments.py over the shipped collection on an emulator/
simulator: open via phyphox://asset=, start, run, stop, download and
validate every export format (tools/validate_export.py), app stays
responsive. Android with emulator-injected sensors. iOS covers the
loadable subset: the driver skips audio-input experiments (they
abort the simulator app) and classifies sensor-unavailable ones as
not-loadable rather than failed - recorded platform difference.
Host-driven - the tag lives in the T1 CI workflow. Uses the
remote-enable and auto-confirm switches.
- id: switch-bypassed-ui
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
Everything the host-controlled switches bypass must still work
WITHOUT them (the switches must never mask a broken user path):
the per-experiment menu toggle enables the remote server with
debug.phyphox.remote / -phyphoxRemote unset; without auto-confirm
the network privacy notice appears and is dismissed by its single
OK - deliberately informational, not a consent gate (no decline
exists: opening an experiment records nothing sensitive, and a
connection before start has nothing meaningful to transmit), so a
decline control turning up is a finding, and the privacy-policy
link works when the experiment sets one; the
photosensitivity warning appears, and the
save-downloaded-experiment offer appears and works both ways;
opening a phyphox:// URL the regular way (not via -phyphoxUrl)
still reaches the app, confirmation dialog included on iOS.
UI-automated - lands with the phase-4 UI suites.
- id: view-snapshots
area: ui
tier: T0
platforms: [android, ios]
status: active
description: >-
Golden-image tests of the non-graph view elements over the
phyphox-docs fixtures/views/ set (values, edits, buttons-toggles,
sliders-dropdowns, info-separator-image), rendered in the
configuration matrix of fixtures/views/README.md (light/dark, two
font scales, phone/tablet width, RTL layout smoke). Android:
Robolectric; iOS: swift-snapshot-testing. Goldens live in the app
repos.
- id: graph-snapshots
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
Golden-image tests of the OpenGL graph rendering over
fixtures/views/graphs-*.phyphox (styles, axes incl. log and fixed
scales, NaN gaps, history, follow-x, color maps, pick labels).
Android: emulator + PixelCopy; iOS: GLKView snapshot in the
simulator. Goldens live in the app repos.
- id: graph-interaction
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
The maximized graph's tools over fixtures/views/graphs-interaction.phyphox:
maximizing and restoring a graph, picking a point (read-out values
and the pick outputs written to their containers, asserted through
the remote API), dragging for the difference and slope read-out,
the linear fit (a = 2, b = 1 on the fixture's line), panning and the
zoom reset, the log-scale menu items on the log graph (the checkmark
follows the toggle, the axis reads in linear units and pans by the
dragged fraction afterwards - iOS 1.2.1 kept clamping a toggled-off
axis to the log limit, freezing pan and zoom on the audio spectrum)
- and every one of those gestures on a graph over empty
containers, which crashed Android 1.2.1 (a null buffer in the pick
handler). Android splits the row: the touch geometry (pick, drag,
pan, pinch, empty buffers) runs at T0 on Robolectric against
GraphView directly, the chrome around it (markers, read-outs, tools
menu, outputs) on the emulator. iOS runs all of it as XCUITest
(GraphInteractionTests) against the simulator, placing taps and
drags through the plot's accessibility value, which reports the
visible axis ranges. Added 2026-09-12 after the 1.2.1 crash review.
Since 2026-10-02 the fixture carries a second view ("Other", one value
element) for the exclusive-navigation row; the tests here work on the
first view only.
- id: apply-zoom-choice
area: ui
tier: T0
platforms: [android, ios]
status: active
description: >-
The rules behind "Keep this view?" when a maximized graph is left with a
zoom: no question when nothing is zoomed, also while a time axis shows
system time (a followX graph following its configured window is not
zoomed); the emphasised button is Keep once the user has kept a zoom
before and Reset otherwise, and the per-axis controls under "More
options…" start from it, a following incremental x axis as "keep and
follow new data"; applying a choice resets an axis with NaN, keeps the
follow fallback of a followX graph, and resetting one axis keeps the
others' zoom (iOS dropped a z zoom when x was reset); each axis section
is titled with the zoomed range in the display unit, formatted like the
tic labels. Robolectric on Android (ApplyZoomChoiceTest), XCTest on iOS
(ApplyZoomChoiceTests).
Implemented and tested on both platforms 2026-10-02.
- id: exclusive-navigation
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
The ways back from a maximized element: the toolbar's back arrow ("‹"
on iOS) and, on Android, the system back first close the maximized
element and leave the experiment only without one; a tab change while
an element is maximized is held back until the element is gone (the
pager swipe is locked meanwhile); on either route a zoomed graph asks
"Keep this view?" first, and Cancel stays on the maximized graph.
Android runs it at T0 on Robolectric against the activity
(ExclusiveNavigationTest, inline XML); iOS as XCUITest against the
simulator over fixtures/views/graphs-interaction.phyphox, whose second
view takes the tab change (ExclusiveNavigationUITests).
Implemented and tested on both platforms 2026-10-02.
- id: graph-empty-state
area: ui
tier: T0
platforms: [android, ios]
status: active
description: >-
An empty plot area explains itself instead of staying blank: "No
data" while the containers hold nothing, "No valid data" when every
point is NaN/infinite or one axis has no values, and "No data in
range" plus an arrow towards the nearest valid point when the data
lies outside the current zoom or a fixed range (non-positive values
on a log axis count as out of range, not invalid). Asserted on the
graph view's classification with data fed directly, including the
arrow direction (left of the plot for a zoom past the end of a line,
above it for a zoom below the data), that the arrow is actually
painted next to the note and nowhere else, and the return to a
normal plot once the zoom is reset. Also the related zero-range
case: all values on an axis identical (0, negative, on a log or time
axis) opens the range around the value with a single tic at it,
and an extend axis keeps its data-derived bounds afterwards. And
the headroom rule decided 2026-09-20: the 5 % headroom is added
only at axis ends the data determines, a fully fixed range and a
zoomed range are shown exactly. Android: Robolectric against
GraphView (GraphEmptyStateTest), where the classification runs in
onDraw. iOS: XCTest against GraphDataManager (GraphEmptyStateTests),
catching the delivered status with a spy delegate. Added
2026-09-20, active the same day.
- id: graph-tic-labels
area: ui
tier: T0
platforms: [android, ios]
status: active
description: >-
Tic labels of a fixed range whose outer tics sit on the plot border:
an interior label is centred on its tic, a border label is moved
inside the plot's width (x, colour scale) or height (y) instead of
hanging into the neighbouring label row or beyond the view, shifted
by at most half its size. Android: Robolectric against GraphView
(GraphTicLabelTest, which also covers the first-frame alignment bug
it uncovered: x labels drawn left-aligned before the first colour
map layout). iOS: XCTest against GraphGridView
(GraphTicLabelTests). Added 2026-09-20.
- id: graph-follow-x
area: ui
tier: T0
platforms: [android, ios]
status: active
description: >-
A graph with followX shows the newest data from the first frame on,
not the minX..maxX window of the attributes until the first zoom
gesture (the iOS bug fixed 2026-09-20). The T1 golden of
fixtures/views/graphs-special/follow-x.phyphox does not guard this
on its own - the iOS golden had been recorded with the bug - so the
initial displayed range is asserted directly. Android: Robolectric
against GraphView (GraphFollowXTest). iOS: XCTest
(GraphFollowXTests). Added 2026-09-20.
- id: view-behavior
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
Interaction tests over the same fixtures: typing into edits
(bounds, signed/decimal, factor), pressing buttons (value writes,
appends, empty-clear, trigger), toggles, both slider types,
dropdown selection - each asserted by its buffer effect through
the remote API. Also the starting state, which no interaction
touches: a container's init beats a control's default, while a
default still fills an empty container, and both survive the
views being rebuilt. Espresso / XCUITest.
- id: app-chrome
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
The screens around the experiment: collection (categories,
ordering, platform hiding), experiment menu and all its actions,
dialogs (timed run, export, remote access incl. privacy note,
save state, delete/rename), settings, about, hints, rotation per
screen, tablet layouts, permission flows (grant AND deny paths).
- id: lifecycle
area: robustness
tier: T1
platforms: [android, ios]
status: active
description: >-
Rotation mid-run, background/foreground with a running experiment,
screen-off with keep-on, kill and relaunch, permission denied
yields a sensible error UI, timed-run edges, rapid start/stop,
opening a second experiment, low-memory smoke, remote access
toggled mid-run.
- id: accessibility-smoke
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
Espresso AccessibilityChecks over the chrome suite on Android;
performAccessibilityAudit() on the main iOS screens. Report-only
at first; escalates to failing after the findings are triaged.
- id: translations-build
area: translations
tier: T0
platforms: [android, ios]
status: active
description: >-
Language handling follows the build: the enabled set (LOCALE_ARRAY
/ Bundle.main.localizations) loads, deviations from the canonical
languages.yml in phyphox-docs and between the platforms are
WARNINGS in the report, never failures. The hard enforcement
against built artifacts is the T2 release check.
- id: translations-ui
area: translations
tier: T1
platforms: [android, ios]
status: active
description: >-
UI regression per build-enabled language: the collection screen, a
representative experiment screen and the critical menus/dialogs
render without truncation/overlap regressions in every enabled
language (snapshot or layout-assertion based, the platform's
choice).
Splittable across CI jobs by a convention both platforms share, so
the shards stay comparable: sort the build's language set, then
honour PHYPHOX_TEST_LANGUAGE_SHARD=i/n (1-based, round-robin over
that sorted list) and PHYPHOX_TEST_LANGUAGES=de,fr (an explicit
subset, for reproducing one language). A code named in
PHYPHOX_TEST_LANGUAGES that the build does not enable FAILS rather
than being skipped - a typo must not quietly remove coverage. With
neither variable set, every enabled language runs, which is what a
local run and a release run do.
- id: device-sensors
host_driven: true # implemented once in phyphox-docs tools/lab/
area: hardware
tier: T2
platforms: [android, ios]
status: active
description: >-
Per-device sensor truth on the seven lab phones, manifest-driven
(tools/lab/devices/): liveness and achieved rate FAIL; the value
plausibility windows (|accel| at rest, gyro near zero, earth-field
magnitude, pressure range, light positive) WARN only - bench
magnetics and stale calibration violate them routinely, and no
plausible app bug changes values while keeping the rate right.
Graceful GPS no-fix indoors. Implemented once host-side in
tools/lab/ - the tag there counts for both platforms.
- id: device-audio
host_driven: true # implemented once in phyphox-docs tools/lab/
area: hardware
tier: T2
platforms: [android, ios]
status: active
description: >-
The self-contained audio loopback (fixtures/audio/loopback.phyphox)
per device: speaker plays 1 kHz while the microphone records, the
FFT peak matches (corrected by the achieved rate), level above the
floor. Covers audio input, output and analysis in one test.
- id: device-experiments
host_driven: true # implemented once in phyphox-docs tools/lab/
area: hardware
tier: T2
platforms: [android, ios]
status: active
description: >-
The full shipped experiment matrix on every lab device through
tools/t1_experiments.py: open via phyphox://asset=, run, stop, all
six export formats validated. Bluetooth experiments load-phase
only (the phase-6 board lab owns their data plane).
- id: device-languages
host_driven: true # implemented once in phyphox-docs tools/lab/
area: translations
tier: T2
platforms: [android, ios]
status: active
description: >-
The release language gate: the BUILT artifact's locale set (aapt
dump badging / the .lproj set) must equal languages.yml exactly -
a missing language or a testing locale left in FAILS. Runs when
the lab host names the release artifacts.
- id: containers-load
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
The container forms through the real intake route
(fixtures/containers/): the multi-experiment zip unpacks to
exactly its two experiments, the resource zip delivers its image
to the experiment, the traversal zip's ../ entry is REJECTED with
nothing written outside the extraction directory (security pin),
and the headerless partial zip (the QR/BLE form) is detected by
its trailing signature, rebuilt and loaded. See
fixtures/containers/README.md.
- id: saved-state-load
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
The saved-state container (docs/saved-states.md, phyphox 1.3.0)
through the real intake route: fixtures/containers/saved-state.zip
restores title, buffers (NaN and Infinity intact, the static
buffer marked filled, the empty one empty, init replaced, the
odd-named one found through data/index.csv), time reference and
resource; a legacy .phyphox state (corpus/generated/
events-state.phyphox) still loads; a count/size mismatch in the
index is refused. See fixtures/containers/README.md.
- id: saved-state-write
area: file-format
tier: T0
platforms: [android, ios]
status: active
description: >-
Writing a state and reading it back round-trips buffers, static
flags, time reference and title; the container has the documented
entry set and CSV dialect, experiment.phyphox byte-identical to the
source, only referenced resources in res/; a legacy state's
experiment file is copied as is. See fixtures/containers/README.md.
- id: saved-state-collection
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
The saved-state flow driven by UI automation: save a state to the
collection, it appears under "Saved states" with the given title and
the app's own colour, reopens with its data, renames (only
meta/state.csv changes), shares as a .zip that is itself a loadable
state, and deletes cleanly including its resources. Hermetic.
- id: save-to-collection
area: ui
tier: T1
platforms: [android, ios]
status: active
description: >-
Saving external experiments to the collection - the flow the
auto-confirm switch deliberately declines - driven by UI
automation accepting the offer: the with-resource zip lands in
the collection with its image extracted into the CRC32-named
resource folder and reopens intact; the multi-experiment zip
saves both via the picker. Hermetic: the saved entries are
deleted at the end.
- id: ble-compat-arduino
area: hardware
tier: T2
platforms: [android, ios]
status: active
description: >-
A release must not break Arduino projects that worked with the
previous one. Seven library examples, flashed UNMODIFIED as
stimulus (the library is not under test and is due its own
session), are driven end to end: discovery with another device
advertising nearby, the experiment transferred over cddf0002,
data decoded, and the phone-to-board direction observed on the
board's own serial output. Four scenarios run on every phone
because Android's BLE behaviour varies by version and vendor; the
rest on the newest phone of each platform. Definitions in
fixtures/ble/scenarios.yml, baselines recorded from the store
release. Release-gated: this is the slowest suite in the lab.
- id: ble-compat-micropython
area: hardware
tier: T2
platforms: [android, ios]
status: active
description: >-
The same guarantee for MicroPython projects, on the ESP32 (the
only board that library supports): all three of its examples, with
randomNumbers and getDataFromSmartphone on every phone and
createExperiment on the newest of each platform.
# ---------------------------------------------------------------- T3 --
# The human checklist, run once per platform at release. These rows carry
# `manual: true`: there is no code to tag, so the checker never asks for
# one, and tools/lab/run.py --merge appends them to the release report as
# a checklist. They are here rather than in a document of their own for
# the same reason everything else is: one list, checked mechanically, so
# a step cannot quietly disappear.
#
# The budget is 30 minutes per platform. Anything that grows past that
# belongs in T2 as automation, not here as more reading.
#
# Two rows were taken out again on 2026-08-31, and both for the same
# reason - a checklist step nobody can actually perform is worse than no
# step at all, because the list then reads as if someone were watching:
#
# * audio-call-interruption (a phone call during an audio experiment).
# Almost none of the lab phones have a mobile subscription, so it would
# be skipped on nearly every run. Audio interruption handling is left to
# user reports and to colleagues testing deliberately on their own
# phones.
#
# store-screenshots was taken out with them and came back on 2026-09-02,
# together with store-texts, once the fastlane lanes were replaced by
# scripts a person runs at the release (tools/store_screenshots.py and the
# two uploaders). Both carry `optional: true`: they are release
# preparation rather than tests, most releases do not need them, and an
# unticked box is a normal outcome. The checklist prints them in their own
# group, outside the 30-minute budget, and RELEASE.md has the procedure.
- id: gps-real-fix
area: sensors
tier: T3
platforms: [android, ios]
status: active
manual: true
description: >-
Outdoors, with a real satellite fix: the GPS experiment shows a
position, an accuracy and a satellite count that settle within a
minute or two, and a short walk moves the coordinates, the speed and
the distance travelled. T2 covers only the indoor no-fix path, which
is a different code path and proves nothing about a working fix.
- id: qr-scan-printed
area: transfer
tier: T3
platforms: [android, ios]
status: active
manual: true
description: >-
Scan the codes on the transfer page
(https://phyphox.org/docs/transferring-experiments/, section "Online
QR-Codes" and "Offline QR-Codes"), once from a printout and once from
a second screen, using the app's own scanner. Five codes are there and
each is a different path: an https link, the same link under the
phyphox:// scheme, one code holding a whole experiment, and a two-code
set that has to be assembled. Each opens an experiment whose title
says which route delivered it, so a wrong result is visible without
reading anything. Covers the camera path, the partial-zip decoding and
the multi-code assembly, none of which a URL opened from the shell
touches. The codes are generated from the example experiments by
tools/generate_qr.py and verified on every docs build; the link codes
need the site to be published.
- id: ble-third-party-device
area: hardware
tier: T3
platforms: [android, ios]
status: active
manual: true
description: >-
Connect to a Bluetooth device nobody wrote for this test - a heart
rate strap, a thermometer, whatever is in the room - and confirm the
scan lists it, an experiment for it loads, and disconnecting is
graceful. The lab's boards all speak the phyphox protocol correctly
by construction; this is the only step where the app meets hardware
that does not.
- id: accessibility-spot-check
area: ui
tier: T3
platforms: [android, ios]
status: active
manual: true
description: >-
With TalkBack or VoiceOver on, reach an experiment from the
collection, start and stop it, and read one value. Not an audit - a
spot check that the main path is operable, which is how the invisible
add-experiment menu was found in 2026-08.
- id: store-screenshots
area: release
tier: T3
platforms: [android, ios]
status: active
manual: true
optional: true
description: >-
Only if the app's appearance changed, or a scene was reworked:
regenerate the store screenshots for every form factor from the
revision being shipped, look at them, and upload them. They are
composed from the shipped experiment files, so a changed experiment
changes the picture. See "Updating the store listing" in RELEASE.md;
uploading is not releasing, and on Play a commit sends the listing
for review.
- id: store-texts
area: release
tier: T3
platforms: [android, ios]
status: active
optional: true
manual: true
description: >-
Only if the listing text changed in phyphox-translation, or a
language gained a translation or a store listing: sync the titles and
descriptions from the store PO files, checking that nothing overruns
Play's limits (30/80/4000 characters) after translation. That
repository is read from and never written to. Apple's subtitle and
keywords have no PO source and stay manual.
- id: permissions-fresh-os
area: ui
tier: T3
platforms: [android, ios]
status: active
manual: true
description: >-
On a factory-fresh install, walk the permission dialogs - location,
microphone, camera, Bluetooth - and confirm each is asked for at the
right moment with a comprehensible reason, and that declining leaves
the app usable. Once per major OS release rather than every release:
the wording and the timing are the OS's, and they change with it.
# --- view groups (file format 1.21, specified 2026-09-25, active 2026-09-26) --
# docs/file-format/views/groups.md is the design. Parsing is covered by
# corpus-valid-load / corpus-invalid-reject once the apps declare 1.21
# (corpus/generated/view-groups.phyphox and the transform-*/stack-*
# invalid fixtures); these rows cover the layout and the runtime binding.
- id: view-groups-layout
area: views
tier: T1
platforms: [android, ios]
status: active
description: >-
Rendering the "Groups" view of corpus/generated/view-groups.phyphox:
the children of a horizontal share the width by weight (2:1:1 in the
fixture); a grid shows one column when the available width is at most
maxWidth and two columns above twice it, with fillLastRow stretching
an odd last child; a hidden child (visibility) gives its space to its
siblings; every child keeps its own natural height and a row is as
tall as its tallest child.
- id: view-stack-transform
area: views
tier: T1
platforms: [android, ios]
status: active
description: >-
Rendering the "Stack" view of corpus/generated/view-groups.phyphox:
all children of a stack share one rectangle sized by the tallest
child, later children draw over earlier ones, the graph's plot area is
transparent and pinned by plotLeft/Top/Right/Bottom; writing a value
into a bound container rotates, moves, scales or fades the wrapped
element with the linear map and clamp applied, an empty or NaN
container leaves the neutral value, and the stack takes no touches
(no maximize, zoom or pick).
- id: view-geometry-draw
area: views
tier: T1
platforms: [android, ios]
status: active
description: >-
Rendering the "Drawing" view of corpus/generated/view-drawing.phyphox
(docs/file-format/views/drawing.md, specified 2026-10-05): a geometry
takes the full width and is width / aspectRatio tall; a rectangle
with cornerRadius, a circle, a line and a ring segment are drawn at
the positions the attributes give, positions as fractions of the box
and lengths as fractions of the width; an area shape is filled only
with color and outlined only with lineColor, a line takes lineColor
or falls back to color; drawing outside the box is clipped; a
geometry in a transform rotates about the origin like an image.
- id: view-scale-draw
area: views
tier: T1
platforms: [android, ios]
status: active
description: >-
Rendering the scales of corpus/generated/view-drawing.phyphox: a
linear scale runs from start to end with tics on the positive side
(right of the direction of travel), a circular one on the arc from
startAngle over sweepAngle, clockwise, with positive distances
outward; major tics at min + k * ticStep, minorTics between them,
values at every valueEvery-th major tic at valueDistance, the label
as "label (unit)" centred at labelPositionX/Y; lineWidth 0, ticLength
0 and valueEvery 0 leave the respective part out; valueOrientation
draws the values upright, along the baseline (tangential) or across
it (radial); writing to a container bound with <input as="min"> or
as="max" re-ranges the tics and values without moving the baseline,
an empty or NaN container leaves the attribute value; a tap on the
label of an untransformed scale inside a stack opens the unit dialog
even with a transformed needle drawn over it (the tap is offered from
the topmost child down, skipping transformed children), a tap
anywhere else on the stack does nothing.
- id: view-scale-units
area: ui
tier: T0
platforms: [android, ios]
status: active
description: >-
What a scale with a unit reference shows under the unit conversion,
checked without rendering on corpus/generated/view-drawing.phyphox:
in the experiment's unit the tics follow ticStep from min and the
values carry as many decimals as the step needs (or precision);
under the imperial setting the Celsius scale shows Fahrenheit values
at automatically chosen tics within the same geometry (the positions
of min and max unchanged, converted with the offset), an explicit
precision follows the precision rule of docs/file-format/units.md;
a text unit is shown verbatim and never converted.
- id: view-vertical-layout
area: views
tier: T1
platforms: [android, ios]
status: active
description: >-
Labels in narrow columns (file format 1.21, specified 2026-09-27):
with verticalLayout a value, edit, toggle, dropdown and a slider with
showValue put the label on its own line above the control, both full
width and left-aligned; without a label the caption and its space are
omitted and the control takes the whole row (value, edit, toggle,
dropdown, slider, graph, camera-gui, depth-gui); an info without a
label keeps one line of height and a button keeps its size.
- id: grid-screen-unit
area: views
tier: T1
platforms: [android, ios]
status: active
description: >-
A grid with maxWidthUnit="screen" (file format 1.21, specified
2026-09-27) counts its columns in multiples of the shorter side of the
app's window: maxWidth="1" shows one column in portrait and two in
landscape on a phone and on a tablet, and a split-screen window is
measured on its own size; an unknown unit is refused
(corpus/invalid grid-unknown-width-unit).
- id: view-align
area: views
tier: T1
platforms: [android, ios]
status: active
description: >-
align on value, edit, toggle, dropdown and slider (file format 1.21,
specified 2026-09-26): with verticalLayout, or without a label, the
label line and the control are aligned left (default), centred or
right; a full-width control keeps its width and aligns its text; in
the side-by-side layout the attribute has no effect; values are
case-insensitive.
- id: view-group-spacing
area: views
tier: T1
platforms: [android, ios]
status: active
description: >-
spacing on vertical, horizontal and grid (file format 1.21, specified
2026-09-26): a gap in text line heights between adjacent visible
children, none at the outer edges; horizontal subtracts the gaps
before sharing the width by weight; grid uses it between columns and
rows and counts it in the column count (smallest n with
(W - (n-1)·s)/n <= maxWidth); default 0 is the current flush layout.
- id: colors-alpha
area: file-format
tier: T1
platforms: [android, ios]
status: active
description: >-
An eight-digit colour (RRGGBBAA) on a graph line, a graph input, a
separator, a value and a map colour stop is blended with what lies
below, in dark and in light mode (the light-mode adjustment keeps the
alpha), and a six-digit colour stays opaque.
# --- camera colour channels (file format 1.21, specified 2026-09-30, planned) --
# spec/input.yml (camera components red, green, blue, linearRed, linearGreen,
# linearBlue) and docs/file-format/input.md "Colour channels" are the design.
# Parsing is covered by corpus-valid-load through
# corpus/generated/camera-rgb.phyphox and camera-rgb-spectrum.phyphox; this
# row covers the values.