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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ because it turns other people's test suites red.

### Added

- **The window in Polish, and a Preferences tab.** The window now speaks the
language your system is set to when it has that language, and English
otherwise - so on a system set to Polish it opens in Polish. Preferences
lets you choose another language. The choice takes effect the next time
the window opens, and Restart now does that at once. It is off while files
are being made, and anything typed on the other tabs is cleared. The
command line, recipes and manifests do not change language. The same tab
says what the window keeps between runs (the output directory, the window
size and the language), shows the folder they are kept in, and Forget
clears them. `tfg-gui --pseudo-language` opens the window with every
sentence longer and accented, to see how a translation will fit before
one exists. It is never saved as a choice.
- **A Windows installer, `tfg-setup_<version>_windows_amd64.msi`, beside the
zip archives.** It installs the window and the command line for every
account on the machine, in `Program Files\Testing Files Generator`, puts
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -666,9 +666,12 @@ not a cut down version: a test compares the two interfaces capability by
capability, and anything only one of them can do has to be declared and
justified rather than quietly drifting apart.

Four screens - one batch, presets, several batches at once, and about. It shows
what a run would cost before writing anything, reports progress while it runs,
and can be cancelled part way without leaving a half written file behind.
Five screens - one batch, presets, several batches at once, preferences, and
about. It shows what a run would cost before writing anything, reports progress
while it runs, and can be cancelled part way without leaving a half written file
behind. The window speaks your system's language when it has it (English and
Polish today) and English otherwise. Preferences lets you choose another, and
says what the window keeps between runs.

<p>
<img src=".github/window-presets.png" width="49%" alt="The Presets screen of the window, with the list of presets open">
Expand Down
3 changes: 3 additions & 0 deletions internal/guard/embeddedassets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ var ownWork = map[string]bool{
"github.com/donislawdev/TestingFilesGenerator/internal/gui/parts heart.svg": true,
// The window's own words.
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text locale/en.json": true,
// The same words in Polish, translated for this project on 2026-09-29 -
// the first language the Preferences screen offers beside English.
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text locale/pl.json": true,
}

// accountFor requires exactly one registry entry to claim a file. None means
Expand Down
15 changes: 13 additions & 2 deletions internal/guard/guitext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,19 @@ var notWords = map[string]string{
"Translating a storage key would lose what was kept the day somebody changed language",
`"windowWidth"`: "the name the window files its width under, never shown",
`"windowHeight"`: "the name the window files its height under, never shown",
`"file"`: "the scheme of a local address, which is a protocol name rather than prose",
`"."`: "the working directory, when the system will not say which one it is",
`"language"`: "the name the window files the chosen language under, never shown. " +
"Translating it would lose the choice the day somebody changed language",
`"--pseudo-language"`: "a flag on the launch line for whoever builds the window, English by D9 like every flag, never translated or shown",
// What each screen IS, as opposed to the word on its tab - see parts.Tab.
// They carry the output directory and the keyboard between screens, so a
// translation that renamed one would split a screen from its own state.
`"generate"`: "what the single batch screen is, the key it is known by between screens, never shown",
`"presets"`: "what the presets screen is, the key it is known by between screens, never shown",
`"recipe"`: "what the several batches screen is, the key it is known by between screens, never shown",
`"preferences"`: "what the preferences screen is, the key it is known by between screens, never shown",
`"about"`: "what the about screen is, the key it is known by between screens, never shown",
`"file"`: "the scheme of a local address, which is a protocol name rather than prose",
`"."`: "the working directory, when the system will not say which one it is",
`"tfg-out"`: "the folder the window offers to write into. A directory name rather than " +
"prose, and translating it would move where somebody's files land",
`". "`: "what joins two sentences the declaration already carries",
Expand Down
27 changes: 23 additions & 4 deletions internal/guard/menushape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestAMenuIsNotDrawnAsWideAsABoxToTypeIn(t *testing.T) {
for _, tab := range allTabs() {
screen := selectTab(t, content, tab)
menus, boxes := menusOn(screen), typingBoxesOn(screen)
if len(menus) == 0 {
if len(menus) == 0 || holdsNoBoxByDesign(t, tab, boxes) {
continue
}
widest := float32(0)
Expand Down Expand Up @@ -173,6 +173,25 @@ func menusOn(screen fyne.CanvasObject) []*parts.Chooser {
return found
}

// holdsNoBoxByDesign says whether a screen is the one whose menus stand beside
// no box to type in, so that a guard comparing a menu with the boxes beside it
// has nothing to compare on it. Preferences holds one menu and no box.
//
// Named rather than skipped whenever a screen comes back with no box: a screen
// that lost its boxes to a renamed part would otherwise pass these guards by
// not looking. Held both ways - the day a box stands on the named screen, the
// name stops being true out loud.
func holdsNoBoxByDesign(t *testing.T, tab string, boxes []*parts.Entry) bool {
t.Helper()
if tab != text.TabPreferences() {
return false
}
if len(boxes) != 0 {
t.Errorf("the %s screen holds %d box(es) to type in and is still named as holding none", tab, len(boxes))
}
return true
}

func typingBoxesOn(screen fyne.CanvasObject) []*parts.Entry {
var found []*parts.Entry
walk(screen, func(o fyne.CanvasObject) {
Expand Down Expand Up @@ -263,12 +282,12 @@ func TestNoMenuIsNarrowerThanTheBoxesItStandsBeside(t *testing.T) {
checked := 0
for _, tab := range allTabs() {
screen := selectTab(t, content, tab)
menus := menusWithAnArchiveOpened(t, screen, tab, canvas)
if len(menus) == 0 {
menus, boxes := menusWithAnArchiveOpened(t, screen, tab, canvas), typingBoxesOn(screen)
if len(menus) == 0 || holdsNoBoxByDesign(t, tab, boxes) {
continue
}
narrowest := float32(0)
for _, box := range typingBoxesOn(screen) {
for _, box := range boxes {
w := box.Size().Width
if w <= 0 {
continue
Expand Down
5 changes: 3 additions & 2 deletions internal/guard/navigation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ func TestMovingBetweenScreensIsTabsAndNotButtons(t *testing.T) {
window.Open(host)

// The order is the order of the list, and About stays last. The two work
// screens that produce files come first, the recipe screen joins them, and
// screens that produce files come first, the recipe screen joins them,
// Preferences follows the work because it changes nothing a run makes, and
// the notice is the one somebody goes looking for rather than passes through.
want := []string{text.TabOneTarget(), text.TabPresets(), text.TabRecipe(), text.TabAbout()}
want := []string{text.TabOneTarget(), text.TabPresets(), text.TabRecipe(), text.TabPreferences(), text.TabAbout()}
got := tabNames(host.content)
if len(got) != len(want) {
t.Fatalf("the window has tabs %v and %v was expected", got, want)
Expand Down
2 changes: 1 addition & 1 deletion internal/guard/notelemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ var librariesLoadedByPath = map[string]approvedOperation{
// of the compiler.
var spawnsAllowed = map[string]approvedOperation{
"internal/gui/again.go": {call: "exec.Command", from: "os.Executable",
why: "starts this program again, by the path os.Executable answers, for the software renderer - see startAgain there"},
why: "starts this program again, by the path os.Executable answers - for the software renderer, and for Restart now on the Preferences screen - see ourselves there"},
}

// withoutRegisteredPathLoads drops the findings a registered file is allowed
Expand Down
181 changes: 181 additions & 0 deletions internal/guard/preferences_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package guard

import (
"strings"
"testing"

"fyne.io/fyne/v2"

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

// The Preferences screen, docs/USTAWIENIA-2026-09-29.md: which language the
// window speaks, and what it keeps between runs.

// polishName is what the list calls Polish, read from the build rather than
// typed here - a guard that wrote "Polski" would pass against a list that
// shows something else.
func polishName(t *testing.T) string {
t.Helper()
for _, l := range text.Languages() {
if l.Tag == "pl" {
return l.Name
}
}
t.Fatal("this build carries no Polish, so there is no second language to choose")
return ""
}

// Choosing a language saves it at once, and Restart now is offered exactly
// while a restart would change what the window speaks.
func TestChoosingALanguageSavesItAndOffersARestart(t *testing.T) {
host := newFakeHost(t)
window.Open(host)
prefs := selectTab(t, host.content, text.TabPreferences())
restart := buttonNamed(prefs, text.ButtonRestart())
if restart == nil {
t.Fatalf("there is no %q button. The screen has: %v", text.ButtonRestart(), buttonNames(prefs))
}
// In the state this is about: English on screen, nothing chosen.
if text.Speaking() != text.English || host.Remembered().Language() != "" {
t.Fatalf("the window speaks %q with %q chosen, not English with nothing chosen",
text.Speaking(), host.Remembered().Language())
}
if !restart.Disabled() {
t.Error("Restart now is offered with nothing chosen that a restart would change")
}

menu := chooserUnder(t, prefs, text.FieldLanguage())
menu.SetSelected(polishName(t))
if got := host.Remembered().Language(); got != "pl" {
t.Errorf("Polish was chosen and %q was saved", got)
}
if restart.Disabled() {
t.Error("Polish was chosen and Restart now stayed off, so the choice can only arrive by closing the window by hand")
}

// Back to the system, which here is English: nothing saved, and nothing a
// restart would change.
menu.SetSelected(text.ChoiceSameAsSystem(text.LanguageName()))
if got := host.Remembered().Language(); got != "" {
t.Errorf("the system was chosen and %q stayed saved", got)
}
if !restart.Disabled() {
t.Error("the language on screen was chosen again and Restart now stayed on")
}
}

// Opening the screen writes nothing - above all when the saved language is
// one this build does not carry. The list shows the system then, and saving
// what it shows would erase the person's choice without a press (untouchable
// rule 7) and the sentence saying it was missing along with it.
func TestOpeningPreferencesChangesNothingSaved(t *testing.T) {
host := newFakeHost(t)
// A real language this build does not carry, so it is parsed and matched
// rather than rejected as a malformed tag.
host.Remembered().RememberLanguage("de")
window.Open(host)
prefs := selectTab(t, host.content, text.TabPreferences())

if got := host.Remembered().Language(); got != "de" {
t.Errorf("a saved choice of de became %q by opening the window", got)
}
if said := textIn(prefs); !strings.Contains(said, text.PreferencesMissing("de")) {
t.Errorf("the screen does not say the saved language is missing from this version:\n%s", said)
}
if got, want := chooserUnder(t, prefs, text.FieldLanguage()).Selected, text.ChoiceSameAsSystem(text.LanguageName()); got != want {
t.Errorf("with the saved language missing the list shows %q, and what the window speaks for it is %q", got, want)
}
}

// Restart now stands down while files are being made, stands up when they are
// done, and closes the window the way the close button does - the run
// stopped, the folder written down - before asking for the new one.
func TestRestartWaitsForTheRunAndClosesTheWayAPersonDoes(t *testing.T) {
host, content, hold := heldScreen(t)
prefs := tabNamed(t, host.content, text.TabPreferences())
chooserUnder(t, prefs, text.FieldLanguage()).SetSelected(polishName(t))
restart := buttonNamed(prefs, text.ButtonRestart())
if restart == nil || restart.Disabled() {
t.Fatal("Polish is chosen and Restart now is not on, so this guard cannot see it stand down")
}

dir := t.TempDir()
fill(t, content, text.FieldOutputDir(), dir)
fill(t, content, text.FieldCount(), "20000")
press(t, content, text.ButtonPreview())
hold.look(func() {
if !restart.Disabled() {
t.Error("Restart now stayed on while the screen was busy - pressing it would close the window and stop the work")
}
if said := textIn(prefs); !strings.Contains(said, text.PreferencesRestartBusy()) {
t.Errorf("the screen does not say why Restart now is off:\n%s", said)
}
})
join(host)
if restart.Disabled() {
t.Fatal("the work ended and Restart now stayed off")
}

press(t, prefs, text.ButtonRestart())
if host.restarts != 1 {
t.Errorf("Restart now asked for %d new windows", host.restarts)
}
if host.closed != 1 {
t.Errorf("Restart now closed the window %d times", host.closed)
}
if got := host.Remembered().Directory(); got != dir {
t.Errorf("the output folder written down at the restart is %q, not %q - the window did not close the way the close button closes it", got, dir)
}
}

// Forget removes what the window keeps, says so, and the close that follows
// writes none of it back.
func TestForgetClearsWhatTheWindowKeepsAndTheCloseWritesNothingBack(t *testing.T) {
host := newFakeHost(t)
kept := host.Remembered()
kept.RememberDirectory(`C:\before`)
kept.RememberSize(fyne.NewSize(900, 700))
kept.RememberLanguage("pl")
window.Open(host)
prefs := selectTab(t, host.content, text.TabPreferences())
if host.kept.dir == "" || host.kept.language == "" {
t.Fatal("nothing was kept before Forget, so this guard would pass on a button that does nothing")
}

press(t, prefs, text.ButtonForget())
if host.kept.forgets != 1 {
t.Errorf("Forget reached the store %d times", host.kept.forgets)
}
if host.kept.dir != "" || host.kept.size != (fyne.Size{}) || host.kept.language != "" {
t.Errorf("after Forget the store still holds %q, %v and %q", host.kept.dir, host.kept.size, host.kept.language)
}
if forget := buttonNamed(prefs, text.ButtonForget()); forget == nil || !forget.Disabled() {
t.Error("Forget can still be pressed after it was, with nothing left to forget")
}
if said := textIn(prefs); !strings.Contains(said, text.PreferencesForgotten()) {
t.Errorf("the screen does not say it forgot:\n%s", said)
}
if got, want := chooserUnder(t, prefs, text.FieldLanguage()).Selected, text.ChoiceSameAsSystem(text.LanguageName()); got != want {
t.Errorf("after Forget the list shows %q, and with nothing chosen the window speaks %q", got, want)
}

// The close button, as a person presses it.
host.intercept()
if host.kept.dir != "" {
t.Errorf("closing the window after Forget wrote the folder %q back", host.kept.dir)
}
// And the size, which the real window writes after the close rather than
// in it. A fresh store writes one - the control, without which a store
// that writes nothing at all would pass - and the window's own store,
// told to forget, does not.
window.Forgetting(host.kept).RememberSize(fyne.NewSize(1, 1))
if host.kept.size != fyne.NewSize(1, 1) {
t.Fatalf("a store that was never told to forget did not write a size, so the next check proves nothing")
}
host.Remembered().RememberSize(fyne.NewSize(800, 600))
if host.kept.size != fyne.NewSize(1, 1) {
t.Errorf("after Forget the window's store wrote a size of %v", host.kept.size)
}
}
13 changes: 11 additions & 2 deletions internal/guard/reachability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func laidOutWindow(t *testing.T) (fyne.CanvasObject, fyne.Canvas) {
// looking at three of the four. Passing by not looking is worse than failing.
// TestEveryTabInTheWindowIsOnTheListGuardsWalk keeps it honest.
func allTabs() []string {
return []string{text.TabOneTarget(), text.TabPresets(), text.TabRecipe(), text.TabAbout()}
return []string{text.TabOneTarget(), text.TabPresets(), text.TabRecipe(), text.TabPreferences(), text.TabAbout()}
}

// What this defends. A button a person can see is a button a person can press.
Expand Down Expand Up @@ -289,7 +289,16 @@ func TestTabbingReachesTheControlsAndSaysInWhatOrder(t *testing.T) {
reachedWords[word.Text()] = true
}
}
if !reachedChrome {
// Every screen but Preferences, by the owner's decision of
// 2026-09-29: that screen holds how the window speaks and what it
// keeps, and asks for nothing. Held both ways, so the day a Donate
// button stands on it this exemption stops being true out loud.
withoutDonate := tab == text.TabPreferences()
if withoutDonate && buttonNamed(screen, text.ButtonDonate()) != nil {
t.Errorf("the %q screen holds a %q button and is still named as the screen without one",
tab, text.ButtonDonate())
}
if !reachedChrome && !withoutDonate {
t.Errorf("the %q button cannot be reached with Tab from the %q screen (UX9)",
text.ButtonDonate(), tab)
}
Expand Down
7 changes: 7 additions & 0 deletions internal/guard/screenpixels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@ func screenScenes() []screenScene {
// every later step - and why it is the first in the list.
{name: "catalogue", page: catalogue.Page},
{name: "about", tab: text.TabAbout()},
// The Preferences screen at rest, and with another language chosen -
// the one state in which its main action is on and the line under it
// says what a restart does.
{name: "preferences", tab: text.TabPreferences()},
{name: "preferences-chosen", tab: text.TabPreferences(), set: func(t *testing.T, s scene) {
menuUnder(t, s.tab, text.FieldLanguage()).SetSelected(polishName(t))
}},
{name: "generate", tab: text.TabOneTarget()},
{name: "generate-empty", tab: text.TabOneTarget(), set: func(t *testing.T, s scene) {
fillField(t, s.tab, text.FieldCount(), "0")
Expand Down
Loading
Loading