// Package test provides utility drivers for running UI tests without rendering to a screen. package test // import "fyne.io/fyne/v2/test" import ( "net/url" "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal" "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/painter" "fyne.io/fyne/v2/theme" ) // ensure we have a dummy app loaded and ready to test func init() { NewApp() } type testApp struct { driver *testDriver settings *testSettings prefs fyne.Preferences propertyLock sync.RWMutex storage fyne.Storage lifecycle fyne.Lifecycle cloud fyne.CloudProvider // user action variables appliedTheme fyne.Theme lastNotification *fyne.Notification } func (a *testApp) CloudProvider() fyne.CloudProvider { return a.cloud } func (a *testApp) Icon() fyne.Resource { return nil } func (a *testApp) SetIcon(fyne.Resource) { // no-op } func (a *testApp) NewWindow(title string) fyne.Window { return a.driver.CreateWindow(title) } func (a *testApp) OpenURL(url *url.URL) error { // no-op return nil } func (a *testApp) Run() { // no-op } func (a *testApp) Quit() { // no-op } func (a *testApp) UniqueID() string { return "testApp" // TODO should this be randomised? } func (a *testApp) Driver() fyne.Driver { return a.driver } func (a *testApp) SendNotification(notify *fyne.Notification) { a.propertyLock.Lock() defer a.propertyLock.Unlock() a.lastNotification = notify } func (a *testApp) SetCloudProvider(p fyne.CloudProvider) { if p == nil { a.cloud = nil return } a.transitionCloud(p) } func (a *testApp) Settings() fyne.Settings { return a.settings } func (a *testApp) Preferences() fyne.Preferences { return a.prefs } func (a *testApp) Storage() fyne.Storage { return a.storage } func (a *testApp) Lifecycle() fyne.Lifecycle { return a.lifecycle } func (a *testApp) Metadata() fyne.AppMetadata { return fyne.AppMetadata{} // just dummy data } func (a *testApp) lastAppliedTheme() fyne.Theme { a.propertyLock.Lock() defer a.propertyLock.Unlock() return a.appliedTheme } func (a *testApp) transitionCloud(p fyne.CloudProvider) { if a.cloud != nil { a.cloud.Cleanup(a) } err := p.Setup(a) if err != nil { fyne.LogError("Failed to set up cloud provider "+p.ProviderName(), err) return } a.cloud = p listeners := a.prefs.ChangeListeners() if pp, ok := p.(fyne.CloudProviderPreferences); ok { a.prefs = pp.CloudPreferences(a) } else { a.prefs = internal.NewInMemoryPreferences() } if store, ok := p.(fyne.CloudProviderStorage); ok { a.storage = store.CloudStorage(a) } else { a.storage = &testStorage{} } for _, l := range listeners { a.prefs.AddChangeListener(l) l() // assume that preferences have changed because we replaced the provider } // after transition ensure settings listener is fired a.settings.apply() } // NewApp returns a new dummy app used for testing. // It loads a test driver which creates a virtual window in memory for testing. func NewApp() fyne.App { settings := &testSettings{scale: 1.0, theme: Theme()} prefs := internal.NewInMemoryPreferences() store := &testStorage{} test := &testApp{settings: settings, prefs: prefs, storage: store, driver: NewDriver().(*testDriver), lifecycle: &app.Lifecycle{}} root, _ := store.docRootURI() store.Docs = &internal.Docs{RootDocURI: root} painter.ClearFontCache() cache.ResetThemeCaches() fyne.SetCurrentApp(test) listener := make(chan fyne.Settings) test.Settings().AddChangeListener(listener) go func() { for { <-listener test.propertyLock.Lock() painter.ClearFontCache() cache.ResetThemeCaches() app.ApplySettings(test.Settings(), test) test.appliedTheme = test.Settings().Theme() test.propertyLock.Unlock() } }() return test } type testSettings struct { theme fyne.Theme scale float32 changeListeners []chan fyne.Settings propertyLock sync.RWMutex } func (s *testSettings) AddChangeListener(listener chan fyne.Settings) { s.propertyLock.Lock() defer s.propertyLock.Unlock() s.changeListeners = append(s.changeListeners, listener) } func (s *testSettings) BuildType() fyne.BuildType { return fyne.BuildStandard } func (s *testSettings) PrimaryColor() string { return theme.ColorBlue } func (s *testSettings) SetTheme(theme fyne.Theme) { s.propertyLock.Lock() s.theme = theme s.propertyLock.Unlock() s.apply() } func (s *testSettings) ShowAnimations() bool { return true } func (s *testSettings) Theme() fyne.Theme { s.propertyLock.RLock() defer s.propertyLock.RUnlock() if s.theme == nil { return theme.DarkTheme() } return s.theme } func (s *testSettings) ThemeVariant() fyne.ThemeVariant { return 2 // not a preference } func (s *testSettings) Scale() float32 { s.propertyLock.RLock() defer s.propertyLock.RUnlock() return s.scale } func (s *testSettings) apply() { s.propertyLock.RLock() listeners := s.changeListeners s.propertyLock.RUnlock() for _, listener := range listeners { select { case listener <- s: default: l := listener go func() { l <- s }() } } }