test: resolve the thin-test item, decline the runner merge
Item 4 of the test-suite review: - Delete TestEmitWithNoObserversIsNoop (no assertion; ranging a nil slice cannot panic) and TestStoreReturnsWiredStore (a getter returning its own field). - Collapse the four TestFilteredJobIndexes* tests into one table-driven TestFilteredJobIndexes, matching TestFilterValue above it. - Replace the TestMainViewBuilds smoke test with TestMainViewRecordStartupAddsHistoryRow, which calls the recordStartup closure for both wordings run.go selects between and asserts the rows reach the History table through its own cell callbacks. Keeps the unique coverage the review identified and adds the !windowShown branch. Item 5 is declined with measurements: the three RunJob tests cost 0.14 s combined, so merging them saves ~90 ms while forcing their three fixtures (including the only Manual trigger) into one. The runner package's runtime is the two timeout tests, not subprocess spawns. go vet and go test -race pass for src/app and src/ui. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+2
-7
@@ -102,7 +102,6 @@ Tests `Service` construction and the state-accessor contract.
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| `TestNewServiceBuildsRuntimePerJob` | Verifies that `NewService` creates a `JobRuntime` entry for every loaded job. |
|
| `TestNewServiceBuildsRuntimePerJob` | Verifies that `NewService` creates a `JobRuntime` entry for every loaded job. |
|
||||||
| `TestJobsReturnsCopy` | Verifies that `Service.Jobs` returns a defensive copy so callers cannot mutate internal state. |
|
| `TestJobsReturnsCopy` | Verifies that `Service.Jobs` returns a defensive copy so callers cannot mutate internal state. |
|
||||||
| `TestStoreReturnsWiredStore` | Verifies that `Service.Store` returns the injected `storage.Store`. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -195,7 +194,6 @@ Tests the event-emission and observer-subscription machinery.
|
|||||||
| Test | Purpose |
|
| Test | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `TestEmitDeliversToAllObserversInOrder` | Verifies that all registered observers receive emitted events in registration order. |
|
| `TestEmitDeliversToAllObserversInOrder` | Verifies that all registered observers receive emitted events in registration order. |
|
||||||
| `TestEmitWithNoObserversIsNoop` | Verifies that emitting an event with no observers does not panic. |
|
|
||||||
| `TestObserverCanReadServiceState` | Verifies that an observer called by `emit` can safely read Service state (jobs, runtimes). |
|
| `TestObserverCanReadServiceState` | Verifies that an observer called by `emit` can safely read Service state (jobs, runtimes). |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -436,10 +434,7 @@ widgets are assembled.
|
|||||||
| `TestFilterValue` | Verifies that `filterValue` returns the correct display string for the current folder filter. |
|
| `TestFilterValue` | Verifies that `filterValue` returns the correct display string for the current folder filter. |
|
||||||
| `TestFolderOptionsAlwaysIncludesSentinels` | Verifies that the folder filter list always starts with "All" and "No folder" sentinel entries. |
|
| `TestFolderOptionsAlwaysIncludesSentinels` | Verifies that the folder filter list always starts with "All" and "No folder" sentinel entries. |
|
||||||
| `TestFolderOptionsAppendsUniqueFolders` | Verifies that folder names from the job list are appended once each, in order, without duplicates. |
|
| `TestFolderOptionsAppendsUniqueFolders` | Verifies that folder names from the job list are appended once each, in order, without duplicates. |
|
||||||
| `TestFilteredJobIndexesAll` | Verifies that the "All" filter returns indexes for every job. |
|
| `TestFilteredJobIndexes` | Table: verifies the "All" filter returns every index, a named folder returns only its own jobs, "No folder" matches empty and blank folder fields, and an empty job list yields no indexes. |
|
||||||
| `TestFilteredJobIndexesByNamedFolder` | Verifies that filtering by a named folder returns only jobs in that folder. |
|
|
||||||
| `TestFilteredJobIndexesNoFolder` | Verifies that the "No folder" filter returns only jobs with an empty folder field. |
|
|
||||||
| `TestFilteredJobIndexesEmptySlice` | Verifies that filtering an empty job slice returns an empty index list. |
|
|
||||||
| `TestNextJobListViewFlipsBothWays` | Verifies the density toggle alternates between detailed and compact from either starting value. |
|
| `TestNextJobListViewFlipsBothWays` | Verifies the density toggle alternates between detailed and compact from either starting value. |
|
||||||
| `TestViewToggleTextNamesTheAction` | Verifies the toggle button is labelled with the action it performs, not the state it is in. |
|
| `TestViewToggleTextNamesTheAction` | Verifies the toggle button is labelled with the action it performs, not the state it is in. |
|
||||||
| `TestJobListViewToggleShrinksRowsAndPersists` | End-to-end: one tap shrinks the row height, relabels the button, and reaches the config; tapping back undoes all three. |
|
| `TestJobListViewToggleShrinksRowsAndPersists` | End-to-end: one tap shrinks the row height, relabels the button, and reaches the config; tapping back undoes all three. |
|
||||||
@@ -528,7 +523,7 @@ Tests main view construction with an injected `*app.Service`.
|
|||||||
| Test | Purpose |
|
| Test | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `TestMainViewFitsTheDefaultWindowSize` | Verifies the assembled content's minimum fits the window size the app asks for, so Fyne never silently widens the window past it. The store's config path is deliberately long, since it was the path label that used to grow the Settings tab. |
|
| `TestMainViewFitsTheDefaultWindowSize` | Verifies the assembled content's minimum fits the window size the app asks for, so Fyne never silently widens the window past it. The store's config path is deliberately long, since it was the path label that used to grow the Settings tab. |
|
||||||
| `TestMainViewBuilds` | Verifies `newMainView` assembles tabs without panic using `fyne.io/fyne/v2/test`. |
|
| `TestMainViewRecordStartupAddsHistoryRow` | Verifies the `recordStartup` closure `newMainView` returns appends the startup receipt to History and redraws the table, with the windowed and tray wordings `run.go` selects between. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -84,26 +84,34 @@ go test -coverpkg=./src/domain,./src/storage,./src/runner,./src/scheduler,./src/
|
|||||||
None of these is wrong; each is close enough to worthless that it should be
|
None of these is wrong; each is close enough to worthless that it should be
|
||||||
either justified or removed. Grouped because they want one decision, not four.
|
either justified or removed. Grouped because they want one decision, not four.
|
||||||
|
|
||||||
- [ ] `TestEmitWithNoObserversIsNoop`
|
- [x] `TestEmitWithNoObserversIsNoop`
|
||||||
([events_test.go:36](../src/app/events_test.go)) — the only test in the
|
([events_test.go:36](../src/app/events_test.go)) — the only test in the
|
||||||
suite with no assertion at all. Ranging over a nil slice cannot panic in
|
suite with no assertion at all. Ranging over a nil slice cannot panic in
|
||||||
Go, so it pins nothing. Delete.
|
Go, so it pins nothing. Delete.
|
||||||
- [ ] `TestStoreReturnsWiredStore`
|
- [x] `TestStoreReturnsWiredStore`
|
||||||
([service_test.go:51](../src/app/service_test.go)) — asserts that a
|
([service_test.go:51](../src/app/service_test.go)) — asserts that a
|
||||||
one-line getter returns its own field. Delete.
|
one-line getter returns its own field. Delete.
|
||||||
- [ ] `TestMainViewBuilds`
|
- [x] `TestMainViewBuilds`
|
||||||
([mainwindow_test.go:72](../src/ui/mainwindow_test.go)) — a smoke test;
|
([mainwindow_test.go:72](../src/ui/mainwindow_test.go)) — a smoke test;
|
||||||
`TestMainViewFitsTheDefaultWindowSize` builds the same view. Its only
|
`TestMainViewFitsTheDefaultWindowSize` builds the same view. Its only
|
||||||
unique coverage is `w.SetContent(content)` and `recordStartup(0, true)`.
|
unique coverage is `w.SetContent(content)` and `recordStartup(0, true)`.
|
||||||
Either fold those two calls into the sizing test and delete this one, or
|
Either fold those two calls into the sizing test and delete this one, or
|
||||||
keep it and say in its comment that `recordStartup` is what it is for.
|
keep it and say in its comment that `recordStartup` is what it is for.
|
||||||
- [ ] `TestFilteredJobIndexesAll` / `ByNamedFolder` / `NoFolder` / `EmptySlice`
|
|
||||||
|
Done as neither: folding an assertionless `recordStartup` call into the
|
||||||
|
sizing test would have put unrelated work inside an F1/F3 regression
|
||||||
|
guard. It became `TestMainViewRecordStartupAddsHistoryRow`, which calls
|
||||||
|
the closure for both wordings `run.go` selects between and asserts the
|
||||||
|
two rows arrive in the History table, read back through the table's own
|
||||||
|
cell callbacks so the refresh is proved too. Same unique coverage, plus
|
||||||
|
the previously uncovered `!windowShown` branch.
|
||||||
|
- [x] `TestFilteredJobIndexesAll` / `ByNamedFolder` / `NoFolder` / `EmptySlice`
|
||||||
([jobs_view_test.go:59-101](../src/ui/jobs_view_test.go)) — four tests
|
([jobs_view_test.go:59-101](../src/ui/jobs_view_test.go)) — four tests
|
||||||
over one small pure function. Collapse into one table-driven test in the
|
over one small pure function. Collapse into one table-driven test in the
|
||||||
style of `TestFilterValue` directly above them; the `EmptySlice` case
|
style of `TestFilterValue` directly above them; the `EmptySlice` case
|
||||||
becomes one row rather than a function.
|
becomes one row rather than a function.
|
||||||
|
|
||||||
## 5. Runtime cost of the runner tests (optional)
|
## 5. Runtime cost of the runner tests — declined, with measurements
|
||||||
|
|
||||||
`TestRunJobLogFileAllHeaders`, `TestRunJobRecordFields`, and
|
`TestRunJobLogFileAllHeaders`, `TestRunJobRecordFields`, and
|
||||||
`TestRunJobWritesLogFile` ([runner_test.go](../src/runner/runner_test.go)) have
|
`TestRunJobWritesLogFile` ([runner_test.go](../src/runner/runner_test.go)) have
|
||||||
@@ -111,9 +119,28 @@ identical coverage profiles but assert three genuinely different things — log
|
|||||||
headers, `RunRecord` field values, and the log file's name and directory. They
|
headers, `RunRecord` field values, and the log file's name and directory. They
|
||||||
are **not** duplicates and should not be deleted on that basis.
|
are **not** duplicates and should not be deleted on that basis.
|
||||||
|
|
||||||
The cost is that each spawns a real subprocess; the `runner` package takes 5.3 s.
|
- [x] Decided: **do not merge them.** The premise was wrong. Per-test timings
|
||||||
If suite wall time becomes a concern, merge them into one `RunJob` call with
|
from `go test -count=1 -v ./src/runner`:
|
||||||
three assertion blocks. Until then, leave them alone.
|
|
||||||
|
| Test | Time |
|
||||||
|
|---|---|
|
||||||
|
| `TestRunJobTimesOut` | 2.10 s |
|
||||||
|
| `TestRunJobZeroTimeoutMeansNoTimeout` | 1.05 s |
|
||||||
|
| `TestRunJobWritesLogFile` | 0.05 s |
|
||||||
|
| `TestRunJobLogFileAllHeaders` | 0.05 s |
|
||||||
|
| `TestRunJobRecordFields` | 0.04 s |
|
||||||
|
|
||||||
|
The three candidates cost 0.14 s combined, so the merge buys back about
|
||||||
|
90 ms. The package's runtime is the two deliberate waits in the timeout
|
||||||
|
tests plus build time — subprocess spawn is not what makes `runner` slow.
|
||||||
|
Against that, the three fixtures differ in ways the assertions read:
|
||||||
|
`TestRunJobWritesLogFile` runs the `Manual` trigger, the other two run
|
||||||
|
`Schedule`, and each uses its own job ID and name. Merging forces one
|
||||||
|
fixture and drops the `Manual` path from the log-header assertions — the
|
||||||
|
exact silent loss this item warned about, for 90 ms.
|
||||||
|
|
||||||
|
If `runner` wall time ever does become a problem, the two timeout tests
|
||||||
|
are where the seconds are.
|
||||||
|
|
||||||
## Explicitly not changing
|
## Explicitly not changing
|
||||||
|
|
||||||
@@ -142,7 +169,7 @@ Recorded here so a later pass does not re-report them:
|
|||||||
2. Item 3 (`itoa`) — independent of everything else.
|
2. Item 3 (`itoa`) — independent of everything else.
|
||||||
3. Item 1 (deletions) — one commit, with the coverage re-run as evidence.
|
3. Item 1 (deletions) — one commit, with the coverage re-run as evidence.
|
||||||
4. Item 4 (thin tests) — needs a judgment call per test.
|
4. Item 4 (thin tests) — needs a judgment call per test.
|
||||||
5. Item 5 — only if suite wall time becomes a problem.
|
5. Item 5 — measured and declined; see the item.
|
||||||
|
|
||||||
Items 1 and 4 change the test inventory, so [TESTS.md](TESTS.md) has to be
|
Items 1 and 4 change the test inventory, so [TESTS.md](TESTS.md) has to be
|
||||||
updated in the same commit. No [CHANGELOG.md](CHANGELOG.md) entry is needed:
|
updated in the same commit. No [CHANGELOG.md](CHANGELOG.md) entry is needed:
|
||||||
|
|||||||
@@ -33,12 +33,6 @@ func TestEmitDeliversToAllObserversInOrder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmitWithNoObserversIsNoop(t *testing.T) {
|
|
||||||
svc := newTestService(nil)
|
|
||||||
// Must not panic with an empty observer list.
|
|
||||||
svc.emit(JobChanged{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observers may read Service state from within OnEvent without deadlocking,
|
// Observers may read Service state from within OnEvent without deadlocking,
|
||||||
// because emit is called outside the state lock.
|
// because emit is called outside the state lock.
|
||||||
func TestObserverCanReadServiceState(t *testing.T) {
|
func TestObserverCanReadServiceState(t *testing.T) {
|
||||||
|
|||||||
@@ -47,11 +47,3 @@ func TestJobsReturnsCopy(t *testing.T) {
|
|||||||
t.Errorf("Service state leaked through Jobs(): name = %q, want %q", again[0].Name, "Original")
|
t.Errorf("Service state leaked through Jobs(): name = %q, want %q", again[0].Name, "Original")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStoreReturnsWiredStore(t *testing.T) {
|
|
||||||
store := &storage.Store{}
|
|
||||||
svc := NewService(store, nil)
|
|
||||||
if svc.Store() != store {
|
|
||||||
t.Error("Store() did not return the wired store")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+27
-32
@@ -56,48 +56,43 @@ func TestFolderOptionsAppendsUniqueFolders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilteredJobIndexesAll(t *testing.T) {
|
func TestFilteredJobIndexes(t *testing.T) {
|
||||||
jobs := []domain.Job{
|
|
||||||
{Folder: "Maintenance"},
|
|
||||||
{Folder: ""},
|
|
||||||
{Folder: "Reports"},
|
|
||||||
}
|
|
||||||
got := filteredJobIndexes(jobs, allFolders)
|
|
||||||
if len(got) != 3 {
|
|
||||||
t.Errorf("allFolders filter: got %d indexes, want 3", len(got))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilteredJobIndexesByNamedFolder(t *testing.T) {
|
|
||||||
jobs := []domain.Job{
|
jobs := []domain.Job{
|
||||||
{Folder: "Maintenance"}, // index 0
|
{Folder: "Maintenance"}, // index 0
|
||||||
{Folder: ""}, // index 1
|
{Folder: ""}, // index 1 — no folder
|
||||||
{Folder: "Maintenance"}, // index 2
|
{Folder: "Maintenance"}, // index 2
|
||||||
{Folder: "Reports"}, // index 3
|
{Folder: "Reports"}, // index 3
|
||||||
|
{Folder: " "}, // index 4 — blank reads as no folder
|
||||||
}
|
}
|
||||||
got := filteredJobIndexes(jobs, "Maintenance")
|
cases := []struct {
|
||||||
if len(got) != 2 || got[0] != 0 || got[1] != 2 {
|
name string
|
||||||
t.Errorf("Maintenance filter: got %v, want [0 2]", got)
|
jobs []domain.Job
|
||||||
|
filter string
|
||||||
|
want []int
|
||||||
|
}{
|
||||||
|
{"all folders", jobs, allFolders, []int{0, 1, 2, 3, 4}},
|
||||||
|
{"named folder", jobs, "Maintenance", []int{0, 2}},
|
||||||
|
{"no folder", jobs, noFolder, []int{1, 4}},
|
||||||
|
{"empty job list", nil, allFolders, nil},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
got := filteredJobIndexes(tc.jobs, tc.filter)
|
||||||
|
if !sameIndexes(got, tc.want) {
|
||||||
|
t.Errorf("%s: filteredJobIndexes(_, %q) = %v, want %v", tc.name, tc.filter, got, tc.want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilteredJobIndexesNoFolder(t *testing.T) {
|
func sameIndexes(got, want []int) bool {
|
||||||
jobs := []domain.Job{
|
if len(got) != len(want) {
|
||||||
{Folder: "Maintenance"}, // index 0 — excluded
|
return false
|
||||||
{Folder: ""}, // index 1 — no folder → included
|
|
||||||
{Folder: " "}, // index 2 — blank → included
|
|
||||||
}
|
}
|
||||||
got := filteredJobIndexes(jobs, noFolder)
|
for i := range got {
|
||||||
if len(got) != 2 || got[0] != 1 || got[1] != 2 {
|
if got[i] != want[i] {
|
||||||
t.Errorf("noFolder filter: got %v, want [1 2]", got)
|
return false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilteredJobIndexesEmptySlice(t *testing.T) {
|
|
||||||
got := filteredJobIndexes(nil, allFolders)
|
|
||||||
if len(got) != 0 {
|
|
||||||
t.Errorf("empty job list should return empty indexes, got %v", got)
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNextJobListViewFlipsBothWays(t *testing.T) {
|
func TestNextJobListViewFlipsBothWays(t *testing.T) {
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.mixdep.ru/mix/gosentry/src/app"
|
"gitea.mixdep.ru/mix/gosentry/src/app"
|
||||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||||
"gitea.mixdep.ru/mix/gosentry/src/storage"
|
"gitea.mixdep.ru/mix/gosentry/src/storage"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/test"
|
"fyne.io/fyne/v2/test"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTestStore builds a Store rooted in a temp directory. It is separate from
|
// newTestStore builds a Store rooted in a temp directory. It is separate from
|
||||||
@@ -69,7 +73,35 @@ func TestMainViewFitsTheDefaultWindowSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMainViewBuilds(t *testing.T) {
|
// historyTable returns the History tab's table. It is the only widget.Table the
|
||||||
|
// main view builds, so the search does not need to know the tab order.
|
||||||
|
func historyTable(t *testing.T, content fyne.CanvasObject) *widget.Table {
|
||||||
|
t.Helper()
|
||||||
|
tabs, ok := content.(*container.AppTabs)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("main view is not the expected AppTabs container")
|
||||||
|
}
|
||||||
|
for _, item := range tabs.Items {
|
||||||
|
found := findFirst(item.Content, func(o fyne.CanvasObject) bool {
|
||||||
|
_, ok := o.(*widget.Table)
|
||||||
|
return ok
|
||||||
|
})
|
||||||
|
if found != nil {
|
||||||
|
return found.(*widget.Table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatal("main view has no history table")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMainViewRecordStartupAddsHistoryRow covers the second return value of
|
||||||
|
// newMainView. run.go calls it once per launch with a different windowShown
|
||||||
|
// flag depending on whether the app started into the tray, and that call is the
|
||||||
|
// only thing that puts the startup receipt into History — so both the wording
|
||||||
|
// and the fact that the table is redrawn are worth pinning. Building the full
|
||||||
|
// tab set and setting it as the window content is a side benefit: no other test
|
||||||
|
// assembles all three tabs together.
|
||||||
|
func TestMainViewRecordStartupAddsHistoryRow(t *testing.T) {
|
||||||
testApp := test.NewApp()
|
testApp := test.NewApp()
|
||||||
defer testApp.Quit()
|
defer testApp.Quit()
|
||||||
|
|
||||||
@@ -80,9 +112,39 @@ func TestMainViewBuilds(t *testing.T) {
|
|||||||
defer svc.Stop()
|
defer svc.Stop()
|
||||||
|
|
||||||
content, recordStartup := newMainView(w, svc)
|
content, recordStartup := newMainView(w, svc)
|
||||||
if content == nil {
|
|
||||||
t.Fatal("newMainView returned nil content")
|
|
||||||
}
|
|
||||||
w.SetContent(content)
|
w.SetContent(content)
|
||||||
recordStartup(0, true)
|
|
||||||
|
table := historyTable(t, content)
|
||||||
|
if rows, _ := table.Length(); rows != 0 {
|
||||||
|
t.Fatalf("history rows before startup = %d, want 0", rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
recordStartup(1500*time.Millisecond, true)
|
||||||
|
recordStartup(20*time.Millisecond, false)
|
||||||
|
|
||||||
|
rows, _ := table.Length()
|
||||||
|
if rows != 2 {
|
||||||
|
t.Fatalf("history rows after two startup records = %d, want 2", rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the rows back through the table's own cell callbacks, which is what
|
||||||
|
// the redraw does; a value only in the events slice would not prove the
|
||||||
|
// table was refreshed with it.
|
||||||
|
cell := table.CreateCell()
|
||||||
|
cellText := func(row, col int) string {
|
||||||
|
table.UpdateCell(widget.TableCellID{Row: row, Col: col}, cell)
|
||||||
|
return cell.(*widget.Label).Text
|
||||||
|
}
|
||||||
|
if got := cellText(0, 2); got != "Application" {
|
||||||
|
t.Errorf("startup row job = %q, want %q", got, "Application")
|
||||||
|
}
|
||||||
|
if got := cellText(0, 3); got != "Started" {
|
||||||
|
t.Errorf("startup row state = %q, want %q", got, "Started")
|
||||||
|
}
|
||||||
|
if got := cellText(0, 4); got != "Window shown in 1.5s" {
|
||||||
|
t.Errorf("windowed startup detail = %q, want %q", got, "Window shown in 1.5s")
|
||||||
|
}
|
||||||
|
if got := cellText(1, 4); got != "Started in tray in 20ms" {
|
||||||
|
t.Errorf("tray startup detail = %q, want %q", got, "Started in tray in 20ms")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user