Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ because it turns other people's test suites red.
it so under every format, so the ellipsis only ever appears in a window
made narrower than a title needs.

- **The switch between the three ways of stating a size is drawn as one
shape.** The chosen way was a sharp rectangle under a rounded border, with
a hairline against its edge and the keyboard ring outside - three
geometries at once. The fill is rounded and sits inside the border now, and
a line stands only between two ways neither of which is chosen.

- **The words in an open list start where the word in the box does.** Every
row kept a column for the tick in front of its words, whether or not
anything in the list was ticked, so the words of a list with nothing chosen
floated a column to the right of the box. The tick stands at the end of the
row now, and the picture of a file kind stays in front where it was.

### Added

- **Every control shows where the keyboard is and answers the pointer.** Every
Expand All @@ -134,6 +146,12 @@ because it turns other people's test suites red.

### Fixed

- **The switch between the three ways of stating a size freezes with the
rest of the form while a run is going.** It stayed live: during a run,
choosing another way rebuilt the size boxes under a form drawn as frozen.
A frozen switch also kept no sign of which way was chosen. Both are
fixed, and the switch thaws with the form when the run ends.

- **The explanation beside a field opens on the first hover, and opens and
closes from the keyboard.** Pointing at the information mark in a fresh
window showed nothing until something else on the screen happened to
Expand Down
136 changes: 136 additions & 0 deletions internal/guard/listwords_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package guard

import (
"testing"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"

"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
)

// The words in an open list start where the word in the box does, and the
// tick stands at the far end of the row.
//
// Reported by the owner from the running window on 2026-09-16: the list of
// formats looked right and the lists of outcomes and rules looked like words
// floating in a rectangle. Measured: every row kept a column for the tick in
// front of its words whether or not anything in the list was ticked, so the
// words of every list stood 36 px to the right of the word in the box - the
// pictures in front of the formats made that look meant, and a list with no
// picture and nothing chosen showed the empty column for what it was (O220).
//
// Asked of two real lists on the screen: one without pictures, where the row's
// words start at the gutter with nothing in front of them, and one with them,
// where the picture stands at the gutter and the tick behind the words.
// Positions are read off rows the list is actually drawing, because a row laid
// out at one width in a probe says nothing about the width the form gives it.
//
// The rule is held against OUR geometry - the gutter, the tick, the picture -
// and the distance to the box's own word is only logged. The first version
// asserted that distance within a step of the scale, and CI turned it red:
// the toolkit draws the box's word 2 px from where the row's word starts on
// the owner's machine and 6 px on the runners (2026-09-16, all three systems),
// because the inset of a Select's RichText is the toolkit's and not a token
// of ours. What the owner saw was 36 px, and that is what the tick column in
// front of the words was.
func TestTheWordsInAnOpenListStartWhereTheWordInTheBoxDoes(t *testing.T) {
cv, content := screenOnACanvas(t)
drv := fyne.CurrentApp().Driver()

for _, tc := range []struct {
field string
pictured bool
}{
{text.FieldDamage(), false},
{text.FieldFormat(), true},
} {
menu := chooserUnder(t, content, tc.field)
menu.Tapped(&fyne.PointEvent{})
cv.Capture()
list := menu.Opened()
if list == nil {
t.Fatalf("pressing the %s menu opened no list", tc.field)
}
boxWord := drv.AbsolutePositionForObject(wordsInTheBox(t, menu)).X

rows := list.DrawnRows()
if len(rows) == 0 {
t.Fatalf("the %s list is drawing no row at all", tc.field)
}
for _, row := range rows {
words, tick, picture := piecesOfARow(t, row)
first := words.Position().X
if tc.pictured {
first = picture.Position().X
}
if first != parts.RowGutter() {
t.Errorf("%s: row %q starts its first piece at %.1f rather than at the gutter (%.1f) - a column stands in front of the words and the list reads as words floating in a rectangle",
tc.field, row.Label(), first, parts.RowGutter())
}
if !tc.pictured {
t.Logf("%s: row %q words at %.1f, the box's word at %.1f (the toolkit's inset, logged and not held)",
tc.field, row.Label(), drv.AbsolutePositionForObject(words).X, boxWord)
}
if tick.Position().X < words.Position().X+words.Size().Width {
t.Errorf("%s: the tick of row %q stands at %.1f, in front of words ending at %.1f - the column it keeps pushes every list's words off the box's word",
tc.field, row.Label(), tick.Position().X, words.Position().X+words.Size().Width)
}
if tc.pictured && picture.Position().X+picture.Size().Width > words.Position().X {
t.Errorf("%s: the picture of row %q reaches %.1f, over words starting at %.1f",
tc.field, row.Label(), picture.Position().X+picture.Size().Width, words.Position().X)
}
}
list.TypedKey(&fyne.KeyEvent{Name: fyne.KeyEscape})
}
}

// wordsInTheBox is the text the closed menu draws, read off its renderer -
// the toolkit draws it through a RichText, and a guard that cannot find it
// says so rather than measuring nothing.
func wordsInTheBox(t *testing.T, menu *parts.Chooser) *canvas.Text {
t.Helper()
for _, o := range test.WidgetRenderer(menu).Objects() {
rich, is := o.(*widget.RichText)
if !is {
continue
}
for _, drawn := range test.WidgetRenderer(rich).Objects() {
if words, is := drawn.(*canvas.Text); is {
return words
}
}
}
t.Fatal("the closed menu draws no text this guard can find, so it cannot say where the word in the box starts")
return nil
}

// piecesOfARow is what one row of a list draws: its words, its tick and its
// picture, the last two told apart by what they show - the tick is the
// toolkit's confirm icon, and the picture is whatever kind the row has.
func piecesOfARow(t *testing.T, row *parts.ListRow) (words *canvas.Text, tick, picture *canvas.Image) {
t.Helper()
for _, o := range test.WidgetRenderer(row).Objects() {
switch drawn := o.(type) {
case *canvas.Text:
words = drawn
case *canvas.Image:
if drawn.Resource != nil && drawn.Resource.Name() == theme.ConfirmIcon().Name() {
tick = drawn
} else if row.Kind() != nil {
picture = drawn
}
}
}
if words == nil || tick == nil {
t.Fatalf("row %q draws no words or no tick, so this guard read the wrong objects", row.Label())
}
if row.Kind() != nil && picture == nil {
t.Fatalf("row %q has a kind and draws no picture for it", row.Label())
}
return words, tick, picture
}
157 changes: 157 additions & 0 deletions internal/guard/segmentface_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package guard

import (
"image/color"
"testing"

"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/theme"

"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
)

// The face of the segmented switch, after the owner looked at it (O219), and
// the two halves of what a frozen form did not do to it (O223).

// The switch between the three ways of stating a size freezes with the rest
// of the form, and thaws with it.
//
// Measured on a render of the batch screen mid run on 2026-09-16: every box
// frozen, the switch above them live. It went on the form through
// Fields.Unlabelled, which built its row and registered nothing, and Freeze
// walked the registry - so a press on "A range" during a run rebuilt the size
// boxes under a form drawn as frozen. The guard for a frozen form asks the
// single batch screen, which has no such switch (O223).
func TestTheSizeWaySwitchFreezesWithTheRestOfTheForm(t *testing.T) {
dir := t.TempDir()
batches, _, _ := screenInAWindowWithHost(t, text.TabRecipe())
entryUnder(t, batches, text.FieldTargetID()).SetText("invoices")
entryUnder(t, batches, text.FieldOutputDir()).SetText(dir)
entryUnder(t, batches, text.FieldSize()).SetText("64kb")
entryUnder(t, batches, text.FieldCount()).SetText("400")
press(t, batches, "Generate")

box := entryUnder(t, batches, text.FieldSize())
if !box.Disabled() {
t.Fatalf("the size box is not frozen during the run, so this guard is asking about a form that never froze. Refusal: %q",
anyRefusal(batches))
}
if !sizeWaySwitch(t, batches).Disabled() {
t.Error("the size box is frozen and the switch above it is not - a press on another way of stating " +
"a size rebuilds the boxes under a form drawn as frozen")
}

cancel := buttonNamed(batches, "Cancel")
if cancel == nil {
t.Fatalf("there is no Cancel button. The screen has: %v", buttonNames(batches))
}
cancel.OnTapped()
if sizeWaySwitch(t, batches).Disabled() {
t.Error("the switch is still frozen after the run stopped, so it never comes back")
}
}

// A frozen switch still shows which way is chosen.
//
// Until 2026-09-16 every fill went transparent when the switch was disabled,
// so a form frozen for a run would have said nothing about which of the three
// ways it was running with. Nobody saw it, because the switch was not being
// frozen at all - the other half of O223.
func TestAFrozenSwitchStillShowsWhichWayIsChosen(t *testing.T) {
ways := []string{"one", "two", "three"}
s := parts.NewSegments(ways, nil)
s.SetSelected("two")
s.Disable()
filled := segmentFills(t, s)
if len(filled) != 1 || filled[0] != 1 {
t.Errorf("the switch is frozen on %q and the filled segments are %v (by position), so it no longer says which way is chosen",
"two", filled)
}
s.Enable()
if filled := segmentFills(t, s); len(filled) != 1 || filled[0] != 1 {
t.Errorf("thawed, the switch fills segments %v rather than the chosen one", filled)
}
}

// The chosen segment's fill stays inside the border, and no rule touches it.
//
// What the owner saw on 2026-09-16 was three geometries at once: a sharp
// rectangle standing out past the arc of a rounded border, a hairline against
// its edge, and the ring outside. The fill is rounded and a border's width
// inside the switch now, and a rule stands only between two segments neither
// of which is the chosen one. Read off the renderer's objects by what they
// are, not by their position in the list - a guard reading by position is
// green the day the order changes.
func TestTheChosenSegmentStaysInsideTheBorderAndNoRuleTouchesIt(t *testing.T) {
ways := []string{"one", "two", "three"}
s := parts.NewSegments(ways, nil)
s.SetSelected("two")
s.Resize(s.MinSize())

var fill *canvas.Rectangle
var rules []*canvas.Rectangle
for _, o := range test.WidgetRenderer(s).Objects() {
rect, is := o.(*canvas.Rectangle)
if !is {
continue
}
switch {
case rect.FillColor == parts.PaletteColour(theme.ColorNameSelection, theme.VariantDark):
fill = rect
case rect.Size().Width == parts.Hairline:
rules = append(rules, rect)
}
}
if fill == nil {
t.Fatal("no segment is filled with the selection colour, so the chosen way is not drawn at all")
}
if len(rules) != len(ways)-1 {
t.Fatalf("%d rules drawn for %d segments, so this guard read the wrong objects", len(rules), len(ways))
}

edge := float32(parts.EdgeWidth())
pos, size, whole := fill.Position(), fill.Size(), s.Size()
if pos.X < edge || pos.Y < edge || pos.X+size.Width > whole.Width-edge || pos.Y+size.Height > whole.Height-edge {
t.Errorf("the chosen fill at %v size %v reaches under the border of a %v switch, so its corner stands out past the border's arc",
pos, size, whole)
}
if fill.CornerRadius <= 0 {
t.Error("the chosen fill has square corners inside a rounded border")
}
// The chosen segment is the second, so the first rule and the second both
// touch it. A third would stand clear, and there is none with three ways.
for i, rule := range rules {
if rule.Visible() {
t.Errorf("rule %d is drawn against the chosen segment's edge - the fill is the boundary there", i)
}
}
s.SetSelected("one")
if !rules[1].Visible() {
t.Error("with the first way chosen the rule between the second and third is hidden, and it stands between two unchosen segments")
}
}

// segmentFills is the position of every segment whose fill is not
// transparent. A fill is told from the border, the ring and the rules by what
// it is: the one rectangle rounded to sit inside the border's own radius.
func segmentFills(t *testing.T, s *parts.Segments) []int {
t.Helper()
var filled []int
at := 0
for _, o := range test.WidgetRenderer(s).Objects() {
rect, is := o.(*canvas.Rectangle)
if !is || rect.CornerRadius != parts.RadiusField-parts.EdgeWidth() {
continue
}
if rect.FillColor != color.Transparent {
filled = append(filled, at)
}
at++
}
if at != len(s.Options) {
t.Fatalf("read %d fills for %d segments, so this guard is not reading the fills", at, len(s.Options))
}
return filled
}
Binary file modified internal/guard/testdata/screens/catalogue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading