From 21f3ea269665929f312b739667b191e627c16ee8 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Mon, 19 Oct 2026 18:48:28 +0200 Subject: [PATCH 1/4] mutation: the menu guards measure every section open and the narrowest window The last four HOLEs of the Sunday mutation runs were all about the width of a menu, and none of them was a guard looking in the wrong place for the reason the earlier analysis gave. A field in the form's grid is as wide as the columns it takes, not as wide as it asks. At the width the window opens at, every menu is a column wide whatever it asks for, so taking the floor out of parts.menuWidth changed nothing the guards could see. The floor only shows in the narrowest window the window allows, where it decides how many columns a menu takes: without it, the damage menu on the single batch screen falls from two columns (265.91 px) to one (124.96), beside boxes of 253.91. inEveryMenuLayout lays every screen out with every section open and at both widths, and asserts both states rather than assuming them: a section that stays shut when its head is pressed, or content that did not take the width it was resized to, stops the guard. The guard about the toolkit's own words also asks whether the width a menu asks for covers the toolkit's minimum - laid out, the menu without its floor was still given one column, 0.73 px wider than the words, so the layout alone could never prove it. The two entries that measured the placeholder in menuWidth are retired. Since NewChooser gives every menu the window's placeholder from the start, measuring it widens every format menu at once (154.73 to 180.23 px), which is one width everywhere. Their guards are listed as not proven by mutation, and the cause they were written for is held at the source by TestAMenuIsOneWidthBeforeAndAfterItIsDrawn. Co-Authored-By: Claude Opus 5.5 --- internal/guard/menushape_test.go | 162 +++++++++++++++++++----- internal/guard/mutation/entries.py | 18 --- internal/guard/mutationcoverage_test.go | 12 ++ internal/gui/parts/ring.go | 8 +- 4 files changed, 150 insertions(+), 50 deletions(-) diff --git a/internal/guard/menushape_test.go b/internal/guard/menushape_test.go index 0688cf70..d61e37eb 100644 --- a/internal/guard/menushape_test.go +++ b/internal/guard/menushape_test.go @@ -1,6 +1,7 @@ package guard import ( + "fmt" "strings" "testing" @@ -219,43 +220,58 @@ func typingBoxesOn(screen fyne.CanvasObject) []*parts.Entry { // puts in by itself - fyne v2.8.2 widget/select.go line 98 substitutes its // default placeholder while the renderer is made - so the only party that knows // what a menu will end up showing is the menu. +// +// Asked twice, and the second half is what the floor in parts.menuWidth is for. +// Laid out, a menu in the form's grid is as wide as the columns it takes rather +// than as wide as it asks, so the room it is given can cover the toolkit's +// words while the width it ASKS for does not - and the first layout that gives +// it exactly what it asks cuts them. Measured on 2026-10-19 with the floor taken +// out: the damage menu asked for 112.86 px against the toolkit's 124.23 and was +// still given 124.96, one column of the narrowest window, so the layout half +// stayed green by 0.73 px and the mutation run scored the floor a HOLE. func TestAMenuIsWideEnoughForTheWordsTheToolkitPutsInIt(t *testing.T) { ourTheme(t) - content, canvas := laidOutWindow(t) checked := 0 tightest, tightestIn := float32(0), float32(0) - for _, tab := range allTabs() { - screen := selectTab(t, content, tab) - menus := menusWithAnArchiveOpened(t, screen, tab, canvas) - buried := underSomethingHidden(screen) - for _, menu := range menus { - room, shown, err := widthShown(menu, buried) + asked := map[*parts.Chooser]bool{} + inEveryMenuLayout(t, func(l menuLayout) { + for _, menu := range l.menus { + needs := menu.MinSize().Width + if !asked[menu] { + asked[menu] = true + if wants := parts.Menu(menu).MinSize().Width; wants < needs { + t.Errorf("a menu of %v on the %s screen asks for %.2f px and the toolkit says it needs"+ + " %.2f to show what is in it, so a layout that gives it what it asks cuts the words off.\n"+ + "What to do: parts.menuWidth is what decides this width.", + menu.Options, l.tab, wants, needs) + } + } + room, shown, err := widthShown(menu, l.buried) if err != nil { - t.Errorf("a menu of %v on the %s screen %v", menu.Options, tab, err) + t.Errorf("a menu of %v on %s %v", menu.Options, l, err) continue } if !shown { continue } - needs := menu.MinSize().Width if room < needs { - t.Errorf("a menu of %v on the %s screen is %.2f px and the toolkit says it needs"+ + t.Errorf("a menu of %v on %s is %.2f px and the toolkit says it needs"+ " %.2f to show what is in it, so the words are cut off in the box that"+ " exists to show them.\n"+ "What to do: parts.menuWidth is what decides this width.", - menu.Options, tab, room, needs) + menu.Options, l, room, needs) } if slack := room - needs; tightest == 0 || slack < tightest { tightest, tightestIn = slack, room } checked++ } - } + }) if checked == 0 { t.Fatal("no menu was laid out, so this guard checked nothing") } - t.Logf("%d menus, all wide enough. The tightest has %.2f px to spare in %.2f", checked, tightest, tightestIn) + t.Logf("%d menus laid out, all wide enough. The tightest has %.2f px to spare in %.2f", checked, tightest, tightestIn) } // And no menu is narrower than the boxes it stands beside. @@ -275,16 +291,17 @@ func TestAMenuIsWideEnoughForTheWordsTheToolkitPutsInIt(t *testing.T) { // and a floor taken from those would have been no floor at all. None is today // - measured 2026-09-16, after the switch became segments - and the rule stays // for the day a hidden box is laid out at nothing again. +// +// In the narrowest window too, which is the only width at which the floor of +// parts.menuWidth shows - see inEveryMenuLayout. func TestNoMenuIsNarrowerThanTheBoxesItStandsBeside(t *testing.T) { ourTheme(t) - content, canvas := laidOutWindow(t) checked := 0 - for _, tab := range allTabs() { - screen := selectTab(t, content, tab) - menus, boxes := menusWithAnArchiveOpened(t, screen, tab, canvas), typingBoxesOn(screen) - if len(menus) == 0 || holdsNoBoxByDesign(t, tab, boxes) { - continue + inEveryMenuLayout(t, func(l menuLayout) { + boxes := typingBoxesOn(l.screen) + if len(l.menus) == 0 || holdsNoBoxByDesign(t, l.tab, boxes) { + return } narrowest := float32(0) for _, box := range boxes { @@ -297,33 +314,120 @@ func TestNoMenuIsNarrowerThanTheBoxesItStandsBeside(t *testing.T) { } } if narrowest == 0 { - t.Fatalf("the %s screen has %d menus and no box to type in that was laid out,"+ - " so there is nothing to compare them against", tab, len(menus)) + t.Fatalf("%s has %d menus and no box to type in that was laid out,"+ + " so there is nothing to compare them against", l, len(l.menus)) } - buried := underSomethingHidden(screen) - for _, menu := range menus { - got, shown, err := widthShown(menu, buried) + for _, menu := range l.menus { + got, shown, err := widthShown(menu, l.buried) if err != nil { - t.Errorf("a menu of %v on the %s screen %v", menu.Options, tab, err) + t.Errorf("a menu of %v on %s %v", menu.Options, l, err) continue } if !shown { continue } if got < narrowest { - t.Errorf("a menu of %v on the %s screen is %.2f px and the narrowest box beside it"+ + t.Errorf("a menu of %v on %s is %.2f px and the narrowest box beside it"+ " is %.2f, so one setting is drawn shorter here than the same setting is"+ " elsewhere in this window.\n"+ "What to do: parts.menuWidth holds a menu to parts.NumericWidth at the least.", - menu.Options, tab, got, narrowest) + menu.Options, l, got, narrowest) } checked++ } - } + }) if checked == 0 { t.Fatal("no screen has a menu, so this guard checked nothing") } - t.Logf("%d menus, none of them narrower than the boxes beside them", checked) + t.Logf("%d menus laid out, none of them narrower than the boxes beside them", checked) +} + +// menuLayout is one screen laid out at one width, while it stands. +type menuLayout struct { + tab string + width float32 + screen fyne.CanvasObject + menus []*parts.Chooser + buried map[fyne.CanvasObject]bool +} + +func (l menuLayout) String() string { + return fmt.Sprintf("the %s screen in a %.0f px window", l.tab, l.width) +} + +// inEveryMenuLayout hands every screen of the window to ask with every section +// on it open, twice: at the width the window opens at and at the narrowest the +// window allows. +// +// Open, because a menu in a shut section is not shown and the guards above +// step over what is not shown: the batch screen's menus of compression, +// encryption, the expected outcome and its reason were measured by none of +// them until 2026-10-19. +// +// Narrowest, because a field in the form's grid is as wide as the columns it +// takes rather than as wide as it asks (parts.Grid), so at the opening width +// every menu is a column wide whatever it asks for and the floor of +// parts.menuWidth cannot show. The mutation run scored the floor's two entries +// HOLE on every Sunday from the first. Measured on 2026-10-19 with the floor +// taken out: at 1000 px every menu kept its 179 or 185 px, and in the +// narrowest window of the single batch screen (580 px) the damage menu fell +// from two columns, 265.91 px, to one, 124.96 - beside boxes of 253.91. +// +// The narrowest window is the window's, and opening a screen's sections can +// widen it for the screens after it, which is what a person who opened them +// gets too. +func inEveryMenuLayout(t *testing.T, ask func(menuLayout)) { + t.Helper() + content, canvas := laidOutWindow(t) + sized, resizable := canvas.(test.WindowlessCanvas) + if !resizable { + t.Fatalf("the window's canvas is a %T, which cannot be resized, so the narrowest window cannot be asked", canvas) + } + opening := canvas.Size() + // The window's own padding, which a window keeps round its content at + // every width - so the narrowest window is the content's least width and + // this much more. + padding := opening.Width - content.Size().Width + for _, tab := range allTabs() { + screen := selectTab(t, content, tab) + menusWithAnArchiveOpened(t, screen, tab, canvas) + openEverySection(t, screen, tab) + for _, width := range []float32{opening.Width - padding, content.MinSize().Width} { + sized.Resize(fyne.NewSize(width+padding, opening.Height)) + content.Refresh() + // In the state this is about, or it proves nothing: a resize the + // window ignored would measure the wide layout twice. + if got := content.Size().Width; got > width+1 || got < width-1 { + t.Fatalf("the window was resized for content %.0f px wide and its content is %.0f px, so this would not measure the layout it names", + width, got) + } + ask(menuLayout{tab: tab, width: width + padding, screen: screen, menus: menusOn(screen), buried: underSomethingHidden(screen)}) + } + sized.Resize(opening) + } +} + +// openEverySection opens every shut section of a screen, the sections that +// only appear inside an opened one included, and refuses a head that stays +// shut when it is pressed - the state is asserted, not assumed. +func openEverySection(t *testing.T, screen fyne.CanvasObject, tab string) { + t.Helper() + for { + opened := 0 + for _, row := range foldRows(screen) { + if row.head.Open() { + continue + } + row.head.Tapped(nil) + if !row.head.Open() { + t.Fatalf("pressing the head of the section %q on the %s screen left it shut", row.title, tab) + } + opened++ + } + if opened == 0 { + return + } + } } // menusWithAnArchiveOpened is every menu of a screen, including the ones that diff --git a/internal/guard/mutation/entries.py b/internal/guard/mutation/entries.py index 225e4c38..45809940 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -9327,15 +9327,6 @@ "", "TestAMenuIsOneWidthBeforeAndAfterItIsDrawn", ), - ( - # The same defect asked on the screens: the format menu is one width - # on the single batch screen and another on the preset screen. - "the format menu is two widths on two screens", - "internal/gui/parts/ring.go", - "var widest float32\n\tfor _, option := range c.Options {", - "widest := fyne.MeasureText(c.PlaceHolder, size, fyne.TextStyle{}).Width\n\tfor _, option := range c.Options {", - "TestOneSettingIsOneMenuWidthOnEveryScreen", - ), ( # Every row's cells slide one column to the right of the names: the # header stands where it stood and the first row does not. @@ -9345,15 +9336,6 @@ "return Column(GapTight, append([]fyne.CanvasObject{Row(append([]fyne.CanvasObject{Clear()}, bodies...)...)}, areas...)...)", "TestATableNamesItsColumnsOnceOverTheFirstRow", ), - ( - # And the two rows of one table come out two widths: the first row's - # menu has been drawn once, the second's has not. - "the first row of a table is wider than the second", - "internal/gui/parts/ring.go", - "var widest float32\n\tfor _, option := range c.Options {", - "widest := fyne.MeasureText(c.PlaceHolder, size, fyne.TextStyle{}).Width\n\tfor _, option := range c.Options {", - "TestEveryRowOfATableStandsUnderTheSameNames", - ), ( # The other direction: the catalogue vouches for a part nobody has. "the catalogue covers a name parts does not export", diff --git a/internal/guard/mutationcoverage_test.go b/internal/guard/mutationcoverage_test.go index e2afa787..cfceab0a 100644 --- a/internal/guard/mutationcoverage_test.go +++ b/internal/guard/mutationcoverage_test.go @@ -68,6 +68,18 @@ var notProvenByMutation = map[string]bool{ // would NOT catch a wrapper put in front of one and stealing the presses. "TestThreeClicksInABoxSelectWhatIsInIt": true, "TestAFreshRunIntoAnEmptyDirectoryStillWorks": true, + // One menu, two widths, measured at two moments: the cause they were + // written for was the toolkit putting its placeholder into a menu at its + // first draw, and that is closed at the source since 2026-10-06 - + // parts.NewChooser gives every menu the window's placeholder, held by + // TestAMenuIsOneWidthBeforeAndAfterItIsDrawn. Their mutation, measuring the + // placeholder in parts.menuWidth, scored HOLE on every Sunday run, and + // rightly: measured on 2026-10-19 it widens every format menu from 154.73 + // to 180.23 px on every screen and in every row at once. With the + // placeholder taken out of NewChooser as well, both stayed green that day, + // so no pair of breaks here reaches them either. + "TestOneSettingIsOneMenuWidthOnEveryScreen": true, + "TestEveryRowOfATableStandsUnderTheSameNames": true, // The property is a package NAME, and no substitution in the tree can // move a package without breaking the build. Every mutation that reaches // the constant it reads also reddens the size guards, which the runner diff --git a/internal/gui/parts/ring.go b/internal/gui/parts/ring.go index 2ba3cb62..3130aa70 100644 --- a/internal/gui/parts/ring.go +++ b/internal/gui/parts/ring.go @@ -328,9 +328,11 @@ func menuWidth(c *Chooser) float32 { // // Whether the placeholder fits is not this function's to answer, and is // answered: TestAMenuIsWideEnoughForTheWordsTheToolkitPutsInIt asks every - // laid out menu whether its width covers the toolkit's own MinSize, which - // is the one measurement that knows the placeholder. The floor at the end - // of this function is what covers it today, with room to spare. + // menu whether the width it asks for here, and the width it is laid out + // at, cover the toolkit's own MinSize, which is the one measurement that + // knows the placeholder. The floor at the end of this function is what + // covers it today: without it the menu of compression levels asks for 90 + // px where the toolkit needs 131 (measured 2026-10-19). // // Since 2026-10-06 every menu is made with the window's own placeholder // (NewChooser), so the toolkit no longer puts one in and the two widths From c382c29cbcd08ff691b0808426fc18e28cafb9e6 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Mon, 19 Oct 2026 18:48:34 +0200 Subject: [PATCH 2/4] build(deps): golang.org/x/text 0.43.0 The Dependabot pull request (#196) was red on one guard: the notices file writes the version by hand and still said 0.42.0. Between the two copies in the module cache only unicode/bidi and secure/bidirule changed. Neither binary links either of them - go list -deps of cmd/tfg and cmd/tfg-gui reaches cases, language, transform and unicode/norm of this module, all four unchanged - so no generated byte and no verdict about two names colliding can move. The mutation entry that names the version in the notices moves with it. Co-Authored-By: Claude Opus 5.5 --- THIRD-PARTY-NOTICES.md | 2 +- go.mod | 2 +- go.sum | 4 ++-- internal/guard/mutation/entries.py | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index f4a5e47f..910b4df6 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -78,7 +78,7 @@ Source: ## golang.org/x/text -Version 0.42.0. Supplies Unicode normalisation, used to decide whether two file +Version 0.43.0. Supplies Unicode normalisation, used to decide whether two file names are one name spelled two ways. ``` diff --git a/go.mod b/go.mod index 016dc501..9eea3188 100644 --- a/go.mod +++ b/go.mod @@ -83,7 +83,7 @@ require ( github.com/goccy/go-yaml v1.19.2 github.com/nicksnyder/go-i18n/v2 v2.6.1 golang.org/x/sys v0.49.0 - golang.org/x/text v0.42.0 + golang.org/x/text v0.43.0 ) require ( diff --git a/go.sum b/go.sum index 5162576d..87161d03 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/guard/mutation/entries.py b/internal/guard/mutation/entries.py index 45809940..96c59ffa 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -6933,10 +6933,11 @@ "a module's own section states a version nobody builds", "THIRD-PARTY-NOTICES.md", # The number here follows the build: 0.40 became 0.41 on 2026-08-28 - # and 0.42 on 2026-09-22 (#112), and each time this pattern went stale - # with it - a SKIP that reads like a proof, which staleness.py names. + # and 0.42 on 2026-09-22 (#112), 0.43 on 2026-10-19, and each time this + # pattern went stale with it - a SKIP that reads like a proof, which + # staleness.py names. + "Version 0.43.0. Supplies Unicode normalisation", "Version 0.42.0. Supplies Unicode normalisation", - "Version 0.41.0. Supplies Unicode normalisation", "TestTheNoticesFileNamesTheVersionsThatAreActuallyBuilt", ), ( From f05048a7df12f8944aca2d134e173798bd633f44 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Mon, 19 Oct 2026 18:48:40 +0200 Subject: [PATCH 3/4] ci: golangci-lint v2.15.0 Reported by the weekly tool versions job. v2.15.0 refuses exhaustive.ignore-enum-types in config verify, which CI runs before the linter, so the setting is ignore-types now, a list of the same patterns. Checked rather than assumed: without it the linter reports seven switches on fyne.KeyName in internal/gui/parts, with it none. A full run on the tree reports no issues. The two mutation entries that name the pinned command move with it. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 ++-- .golangci.yml | 6 ++++-- internal/guard/mutation/entries.py | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edb7a962..e2ba8fde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -858,7 +858,7 @@ jobs: # this refuses. A key that does not exist, or a value outside what the # schema allows, is otherwise ignored in silence - and a linter reading # a setting nobody applied is a gate that reports what it feels like. - run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0 config verify + run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 config verify - name: golangci-lint # Run rather than imported, so it never enters go.mod and the @@ -872,7 +872,7 @@ jobs: # .golangci.yml. Measured before switching this on: zero findings from # ineffassign and one from misspell, which turned out to be a comment # written in the wrong language rather than a typo. - run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0 run --build-tags "$(cat .github/build-tags)" ./... + run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 run --build-tags "$(cat .github/build-tags)" ./... semgrep: name: semgrep diff --git a/.golangci.yml b/.golangci.yml index d7528926..1ca9c8bc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -114,8 +114,10 @@ linters: default-signifies-exhaustive: true # fyne.KeyName has 70 members, so a switch on a keystroke would have to # list every key on the keyboard to satisfy this. Somebody else's enum, - # and not one we can be complete about. - ignore-enum-types: "KeyName$" + # and not one we can be complete about. A list since v2.15.0, which + # refuses the old ignore-enum-types in config verify. + ignore-types: + - "KeyName$" gosec: # Every entry is a measurement, not a preference. The count beside each diff --git a/internal/guard/mutation/entries.py b/internal/guard/mutation/entries.py index 96c59ffa..795754f2 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -6527,15 +6527,15 @@ "CI stops verifying the linter configuration, so a key the schema " "refuses is ignored in silence", ".github/workflows/ci.yml", - "golangci-lint@v2.14.0 config verify", - "golangci-lint@v2.14.0 version", + "golangci-lint@v2.15.0 config verify", + "golangci-lint@v2.15.0 version", "TestTheWorkflowRunsTheLinterAndVerifiesItsConfiguration", ), ( "CI reads the linter configuration and never runs it against the tree", ".github/workflows/ci.yml", - 'golangci-lint@v2.14.0 run --build-tags "$(cat .github/build-tags)" ./...', - "golangci-lint@v2.14.0 help", + 'golangci-lint@v2.15.0 run --build-tags "$(cat .github/build-tags)" ./...', + "golangci-lint@v2.15.0 help", "TestTheWorkflowRunsTheLinterAndVerifiesItsConfiguration", ), ( From 93894a78f439277f84837540a4c1082bc73ae98f Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Mon, 19 Oct 2026 19:15:35 +0200 Subject: [PATCH 4/4] mutation: the narrowest window asks the canvas through driver/software staticcheck (SA1019) refuses test.WindowlessCanvas, which fyne v2.8.2 keeps only as a deprecated alias of driver/software.WindowlessCanvas - the same type under its current name. Co-Authored-By: Claude Opus 5.5 --- internal/guard/menushape_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/guard/menushape_test.go b/internal/guard/menushape_test.go index d61e37eb..949a0999 100644 --- a/internal/guard/menushape_test.go +++ b/internal/guard/menushape_test.go @@ -7,6 +7,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/software" "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/theme" @@ -379,7 +380,7 @@ func (l menuLayout) String() string { func inEveryMenuLayout(t *testing.T, ask func(menuLayout)) { t.Helper() content, canvas := laidOutWindow(t) - sized, resizable := canvas.(test.WindowlessCanvas) + sized, resizable := canvas.(software.WindowlessCanvas) if !resizable { t.Fatalf("the window's canvas is a %T, which cannot be resized, so the narrowest window cannot be asked", canvas) }