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

### Changed

- **`Generate` means the same thing on every screen.** The `Several batches`
screen used to open with `Batch name` and `Size` empty under a red star
each, so the button that works straight away on `Single batch` turned you
down on the third tab. The first batch now arrives with the same two values
the single batch screen has always had, `files` and `10mb`, and the summary
at the foot of the screen says what the run would come to instead of only
where it would go. Nothing else on that screen is filled in: a count, a kind
of case and an expected outcome left alone still reach the run unstated,
which is what lets the manifest record that you did not state them. A batch
you add yourself still arrives empty, because two batches under one name is
refused.

- **The first screen says what this tool gives you that a file generator does
not.** The sentence under `Single batch` read "Files of one format and one
size, as many as you need", which describes the mechanism. It now names the
manifest and what it is for.

- **The window is painted from one system rather than from colours chosen one
at a time.** Every surface and every piece of text now comes from a single
ladder: one hue, an even step between one surface and the next, and one
Expand Down Expand Up @@ -206,6 +223,29 @@ because it turns other people's test suites red.

### Added

- **The window says where the manifest went, and opens it.** A finished run
used to say `3 files written.` and nothing else, while the same run from the
command line printed the path of the manifest beside the count. The line now
names the file - `3 files written. Manifest: manifest.json` - and a second
button, `Open manifest`, stands beside `Open folder` for as long as there is
a record to open. The manifest is what carries the expected outcome of every
file, its hash and the seed it was made from, so it is the part of a run a
test suite reads. Neither button appears after a preview, because a preview
writes nothing.

- **The About screen says what to do with the program, in three steps.** It
opened with one sentence about what this tool is and ran straight into the
licence notice. A short section above the licence now says how to get from
an empty window to files and a manifest a test can read.

- **The switch that chooses how a batch states its size has a name.** On
`Several batches` the row of `One size | A range | Around a limit` stood
between two fields with nothing over it, so what it was about had to be
guessed from its neighbours. It reads `How the size is given` now, with the
same explanation button every other setting has - and that explanation says
what each of the three does, including `Around a limit`, which makes three
files: one byte under the limit, one on it, one over.

- **The palette is something you can open.** `tfg --catalogue`, the hidden
screen that shows every control the window is built from, now ends with the
palette itself: every colour, what it is for, the value it holds and the
Expand Down
98 changes: 98 additions & 0 deletions internal/guard/aboutuse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package guard

import (
"strings"
"testing"

_ "github.com/donislawdev/TestingFilesGenerator/internal/format/all"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
"github.com/donislawdev/TestingFilesGenerator/internal/recipe"
)

// The screen that says what this program is also says what to do with it.
//
// Counted off the stored screen on 2026-09-22: the thesis had one sentence and
// everything under it was the licence notice and the list of what the binary
// carries. Four fifths of the one screen somebody opens to find out what they
// have answered a question about redistribution - a real question, and not the
// one anybody has first. What to do with the program was on no screen in the
// window at all.

// TestTheAboutScreenSaysHowToUseTheProgram.
//
// Three questions, and the third is the one that makes the other two worth
// asking: the steps have to stand ABOVE the licence. A section carrying the
// same words at the bottom of a page of notices is the defect with a heading
// on it, and a guard that only asked whether the words were somewhere on the
// screen would be green for it.
func TestTheAboutScreenSaysHowToUseTheProgram(t *testing.T) {
content, _ := laidOutWindow(t)
about := tabContent(t, content, text.TabAbout())

steps := text.HowToUseSteps()
if len(steps) == 0 {
t.Fatal("the window offers no steps at all, so there is nothing to look for")
}

said := shownText(about)
for _, step := range steps {
if !strings.Contains(said, step) {
t.Errorf("the About screen does not say %q.\nIt says:\n%s", step, said)
}
}

heading, ok := labelBox(about, text.SectionHowToUse())
if !ok {
t.Fatalf("the About screen has no section headed %q, so the steps are loose on a page of notices",
text.SectionHowToUse())
}
licence, ok := labelBox(about, text.SectionLicence())
if !ok {
t.Fatalf("the About screen has no section headed %q, so this guard cannot ask which comes first",
text.SectionLicence())
}
if heading.Y >= licence.Y {
t.Errorf("%q starts at y=%.0f and %q at y=%.0f, so what to do with this program is under "+
"the licence rather than over it",
text.SectionHowToUse(), heading.Y, text.SectionLicence(), licence.Y)
}

// And the one sentence that was already there is still there. The steps
// are an addition rather than a replacement, and a "fix" that swallowed
// the thesis would be the screen losing the thing it is for.
if !strings.Contains(said, text.AboutTagline()) {
t.Errorf("the About screen no longer says what this program is:\n%s", said)
}
}

// TestTheStepsNameEveryOutcomeTheManifestCanRecord.
//
// The third step tells somebody what they will find in the manifest, which
// makes it a list copied out of a closed set - and a list copied by hand that
// nothing compares with its source goes stale on green. The set is
// recipe.Outcomes(), four values today, and the first version of that step
// named two of them, invented a third word for one ("turn it away" for
// reject) and left sanitize out altogether. Nothing in the tree would have
// said so.
//
// Read from the REGISTRY rather than from a list here, so a fifth outcome is
// caught by being declared. That is the whole reason this is a guard and not
// a proofread.
func TestTheStepsNameEveryOutcomeTheManifestCanRecord(t *testing.T) {
content, _ := laidOutWindow(t)
about := tabContent(t, content, text.TabAbout())
said := shownText(about)

outcomes := recipe.Outcomes()
if len(outcomes) < 2 {
t.Fatalf("the recipe package declares %d outcome(s), which is too few for this to compare anything", len(outcomes))
}
for _, outcome := range outcomes {
if !strings.Contains(said, outcome) {
t.Errorf("the manifest can record outcome %q and the About screen never uses that word, "+
"so somebody reading the steps meets it for the first time in the JSON.\nThe screen says:\n%s",
outcome, said)
}
}
t.Logf("%d outcome(s) named on the screen: %v", len(outcomes), outcomes)
}
86 changes: 86 additions & 0 deletions internal/guard/controlnames_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package guard

import (
"testing"

"fyne.io/fyne/v2"

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

// Nothing a person can operate stands on the form without a name over it.
//
// The switch between the three ways of stating a size was the one that did,
// from the day it replaced three radio circles until 2026-09-23. Every box on
// that screen carried a name in the column and this stood between two of them
// with nothing, so what it was about came from what happened to sit next to
// it - which is guessing, and which is the one control the rest of the screen
// teaches somebody not to expect.

// TestEveryControlOnTheFormStandsUnderAName walks the batch screen and asks
// every switch, box and menu on it whether the thing above it is a name.
//
// Read off the FORM rather than from a list of what should be named, which is
// the difference between this and a guard that would have stayed green: a
// control added tomorrow with no name is caught by being on the screen, and no
// list has to be remembered. The shape asked about is the one the window
// builds - a field is a container laid out as one, and its first thing is the
// name (parts.IsField) - so a control that went on some other way is not
// named by definition and is reported by position.
func TestEveryControlOnTheFormStandsUnderAName(t *testing.T) {
batches, _, _ := screenInAWindowWithHost(t, text.TabRecipe())

// The one this is really about is on the screen, or the walk below proves
// nothing by finding nothing.
switchOnIt := sizeWaySwitch(t, batches)

named := map[fyne.CanvasObject]string{}
walk(batches, func(obj fyne.CanvasObject) {
field, ok := obj.(*fyne.Container)
if !ok || !parts.IsField(field) || len(field.Objects) < 2 {
return
}
head, is := headingOf(field.Objects[0])
if !is || head == "" {
return
}
walk(field.Objects[1], func(inner fyne.CanvasObject) { named[inner] = head })
})

if got, is := named[switchOnIt]; !is {
t.Errorf("the switch that chooses between %q, %q and %q stands on the form with no name over it, "+
"so the only thing saying what it is about is what happens to be beside it",
text.SizeWayExact(), text.SizeWayRange(), text.SizeWayBoundary())
} else if got != text.FieldSizeWay() {
t.Errorf("the switch between the three ways of stating a size stands under %q and the window calls it %q",
got, text.FieldSizeWay())
}

// And it is the whole class rather than the one case. Anything a person
// can type into, choose from or press on the form is asked the same
// question - see the doc above for why this is walked and not listed.
walk(batches, func(obj fyne.CanvasObject) {
if !operable(obj) || !obj.Visible() {
return
}
if _, is := named[obj]; !is {
t.Errorf("a %T is on the form with no name over it", obj)
}
})
}

// operable is a control somebody works with, as opposed to the words, the
// rules and the surfaces around one.
//
// Named by type rather than by "anything focusable", because a button is
// focusable and buttons say what they do on their own face - the bar at the
// foot of the screen is full of them and none belongs to the column of names.
func operable(o fyne.CanvasObject) bool {
switch o.(type) {
case *parts.Entry, *parts.Chooser, *parts.Toggle, *parts.Segments:
return true
}
return false
}
135 changes: 135 additions & 0 deletions internal/guard/manifestoffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package guard

import (
"os"
"path/filepath"
"strings"
"testing"

_ "github.com/donislawdev/TestingFilesGenerator/internal/format/all"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
)

// What a finished run says about the record it left, and the way to open it.
//
// The command line has printed "manifest: <path>" since there was a manifest,
// and the window said "3 files written." and nothing else - so the one thing
// this tool makes that other generators do not was, from a window, a file you
// found in the folder afterwards and wondered about. D1 asks for parity
// between the two surfaces and this is the kind that goes quietly: nothing the
// engine can do was missing, only the sentence about it.

// TestAFinishedRunNamesTheManifestAndOffersToOpenIt.
//
// Neither half asks the window what it decided. The NAME is compared against
// the file that is actually on the disk after the run, and the BUTTON is
// pressed and its destination is compared against that same file - so a
// screen that names a manifest it did not write, or opens one it did not
// name, is red. A guard that read text.ManifestNamed and looked for it on the
// screen would be comparing the window with itself.
func TestAFinishedRunNamesTheManifestAndOffersToOpenIt(t *testing.T) {
dir := t.TempDir()
host, content, _ := keyedWindow(t)
screen := selectTab(t, content, text.TabOneTarget())

// Nothing has run, so there is nothing to open. Asked about what is SHOWN
// rather than what is in the tree: the button is built with the bar and
// hidden, so a guard that only looked for it would find it every time.
if shownButton(screen, text.ButtonOpenManifest()) != nil {
t.Fatal("the manifest button is on the screen before anything was written, " +
"so it points at a file that need not exist")
}

entryUnder(t, screen, text.FieldOutputDir()).SetText(dir)
entryUnder(t, screen, text.FieldSize()).SetText("1kb")
entryUnder(t, screen, text.FieldTargetID()).SetText("done")
press(t, screen, text.ButtonGenerate())
waitForManifest(t, host, dir)
join(host)

// The file the run really wrote, found by looking rather than by naming:
// the manifest's name is a setting, so a guard holding the default would
// stop asking anything the day a screen wrote it somewhere else.
written := manifestIn(t, dir)

if said := shownText(screen); !strings.Contains(said, filepath.Base(written)) {
t.Errorf("the run wrote %s and the screen never names it. It says:\n%s\n"+
"Somebody generating from this window has no way to learn they got a manifest at all",
filepath.Base(written), said)
}

button := shownButton(screen, text.ButtonOpenManifest())
if button == nil {
t.Fatal("the run wrote a manifest and there is no way to open it")
}
button.OnTapped()
if host.fileCount != 1 {
t.Errorf("the manifest button was pressed and the desktop was asked to open a file %d times", host.fileCount)
}
if host.file != written {
t.Errorf("the button opens %q and the manifest of this run is %q", host.file, written)
}
// And the folder button is untouched by any of this, because two buttons
// that both open the folder would be one button drawn twice.
if host.folderCount != 0 {
t.Errorf("pressing the manifest button asked the desktop for a folder %d times", host.folderCount)
}
}

// TestAPreviewNamesNoManifestAndOffersNone is the other end of it.
//
// A preview goes through the whole of planning with nothing written, so there
// is no record to name and no file to open - and a screen that said there was
// would be sending somebody to a file the run deliberately did not create.
// This is also the half that fails if the sentence is ever attached to the
// outcome rather than to the saving.
func TestAPreviewNamesNoManifestAndOffersNone(t *testing.T) {
dir := t.TempDir()
host, content, _ := keyedWindow(t)
screen := selectTab(t, content, text.TabOneTarget())

entryUnder(t, screen, text.FieldOutputDir()).SetText(dir)
entryUnder(t, screen, text.FieldSize()).SetText("1kb")
entryUnder(t, screen, text.FieldTargetID()).SetText("planned")
press(t, screen, text.ButtonPreview())
join(host)

// The preview really did go through, or the two questions below are being
// asked of a screen where nothing happened at all.
if said := shownText(screen); !strings.Contains(said, "1 file") {
t.Fatalf("the preview said nothing about what the form comes to, so this guard is "+
"asking about a press that did not work. It says:\n%s\nRefusal: %q", said, anyRefusal(screen))
}
if entries, err := os.ReadDir(dir); err != nil || len(entries) != 0 {
t.Fatalf("a preview wrote into %s: %v (err %v)", dir, whatIsIn(dir), err)
}

if shownButton(screen, text.ButtonOpenManifest()) != nil {
t.Error("a preview wrote nothing and the screen offers to open a manifest")
}
if said := shownText(screen); strings.Contains(said, text.ManifestNamed("")) {
t.Errorf("a preview wrote nothing and the screen names a manifest:\n%s", said)
}
}

// manifestIn is the one record a run left in a directory, found on the disk.
//
// It refuses to guess where there is more than one, because the question this
// helper answers - which file did the window just name - has no answer then.
func manifestIn(t *testing.T, dir string) string {
t.Helper()
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("reading %s: %v", dir, err)
}
var found []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".json") {
found = append(found, filepath.Join(dir, e.Name()))
}
}
if len(found) != 1 {
t.Fatalf("%s holds %d files that could be the manifest: %v", dir, len(found), whatIsIn(dir))
}
return found[0]
}
Loading
Loading