1
"""Wrapper code to help when scripting Evolution
3
Author: David Malcolm <dmalcolm@redhat.com>"""
5
__author__ = 'David Malcolm <dmalcolm@redhat.com>'
7
from dogtail.tree import *
8
from dogtail.procedural import *
9
from dogtail.utils import run
10
from os import environ, path, remove
12
from dogtail.distro import *
13
from dogtail.version import Version
15
from dogtail.apps.categories import *
17
# Evolution folder browser (GtkTreeView?) seems to be implemented as a list of role='table cell' children (the rows), with each having a list of 'table cell' children (the table cells)
18
# The table rows have NODE_CHILD_OF relations (with paths, in at-poke)
21
# Use the gettext translations sniffed from the package db:
23
#dogtail.i18n.loadTranslationsFromPackageMoFiles('evolution')
24
dogtail.i18n.loadTranslationsFromPackageMoFiles('evolution-connector')
26
# App-specific wrapper classes:
28
class EvolutionApp(Application, EmailClient):
30
Useful test hooks for Evolution testcases
33
Application.__init__(self, root.application("evolution"))
35
self.evoVersion = packageDb.getVersion("evolution")
36
print "Evolution version %s"%self.evoVersion
38
if self.evoVersion>=Version([1,5,0]):
39
self.edsVersion = packageDb.getVersion("evolution-data-server")
40
print "evolution-data-server version %s"%self.edsVersion
42
self.edsVersion = None
44
# The name of the gtkhtml package(s) for the various versions of Evolution varies between distributions:
45
self.gtkhtmlVersion = None
47
if isinstance(distro, RedHatOrFedora):
48
self.gtkhtmlPackageName = "gtkhtml3"
50
raise "You need to implement appropriate logic here to get the name of the gtkhtml package for your distribution for this version of Evolution"
51
self.gtkhtmlVersion = packageDb.getVersion(self.gtkhtmlPackageName)
52
print "%s version %s"%(self.gtkhtmlPackageName, self.gtkhtmlVersion)
54
def getConfigMenuItem(self):
56
Get the menu item that triggers the Settings/Preferences dialog,
57
handling version-specific differences
59
if self.evoVersion<Version([2,1,0]):
60
return self.menu("Tools").menuItem("Settings...")
62
return self.menu("Edit").menuItem("Preferences")
64
def doPasswordDialog(self, password, rememberPassword):
66
Utility function for dealing with Evolution's password dialog. It
67
finds the dialog, then fills in the password, setting the 'Remember
68
Password' checkbox accordingly. It then clicks on OK
70
passwdDlg = self.child(roleName="alert", recursive=False, debugName="passwordDlg")
71
passwdDlg.child("Remember this password").setCheckbox = rememberPassword
72
passwdDlg.child(roleName="password text").passwordText = password
73
passwdDlg.child("OK").click()
75
def getSelectFolderDialog(self):
77
Utility function to get the 'Select folder' dialog, wrapped
79
return EvolutionSelectFolderDialog(self.dialog("Select folder"), self)
81
#FIXME: generalize this to handle arbitrary error dialogs, and allow a non-fatal version where we check that an error that was meant to occur did occur?
82
#FIXME: maybe allow installation of error detection hooks into the agent, and have it do it after every "action"??? (more complicated?)
83
def detectAuthFailure(self):
85
Detect an 'Evolution Error' dialog, raising an exception if one is
86
found. The text of the dialog is scraped into the exception.
88
errorDlg = self.child("Evolution Error", recursive=False)
90
raise "AuthenticationError: %s"%errorDlg.child(roleName="label").text
92
#FIXME: finish the import hooks
94
def __doImportFirstPage(self):
96
Open the File->Import wizard and navigate to the second page
98
self.menu("File").menuItem("Import...").click()
99
importAssistant = Wizard(self.child("Evolution Import Assistant", recursive=False), "Import Assistant")
100
importAssistant.clickForward()
101
return importAssistant
103
def __doImportFromOtherProgram(self):
104
importAssistant = self.__doImportFirstPage()
105
importAssistant.child("Import data and settings from older programs").click()
106
importAssistant.clickForward()
107
return importAssistant
109
def __doImportFromSingleFile(self, filename, filetype):
110
importAssistant = self.__doImportFirstPage()
111
importAssistant.child("Import a single file").click()
112
importAssistant.clickForward()
114
importAssistant.child(label="Filename:").child(roleName="text").text = filename
115
importAssistant.child(label="File type:").combovalue=filetype
116
importAssistant.clickForward()
118
return importAssistant
120
def importSingleEmail(self, filename, useSubfolder=False):
122
Test hook to test importing a single email. Imports the email into a
123
local folder (either the local Inbox, or creates specially-purpose
124
subfolder inside the local Inbox), then views that email (and then
125
tests that Evolution is still running)
127
FIXME: not fully implemented yet
130
# Tested on Evolution 2.0.4 and Evolution 2.4.0:
131
# FIXME: what's the _exact_ value for this version test?
132
if self.evoVersion>=Version([2,4.0]):
133
filetype = "Berkeley Mailbox (mbox)"
135
filetype = "MBox (mbox)"
137
importAssistant = self.__doImportFromSingleFile(filename, filetype)
140
# Unfortunately this doesn't yet have a labelling relationship (in evolution-data-server 1.4.0)
141
#importAssistant.child(label="Destination folder:").click()
142
# So we get it by its label:
143
importAssistant.child("Inbox").click()
145
selectFolderDlg = self.getSelectFolderDialog()
146
folderName = "test folder for %s"%agent.testRunId
147
selectFolderDlg.newInboxSubfolder(folderName)
148
# FIXME: the above can fail if the testrun ID contains characters that can't be a folder name
150
importAssistant.clickForward()
151
importAssistant.child("Import").click()
153
# FIXME: now view it and see if we're still alive
154
# use PrintPreview as well
156
def __doIdentityPage(self, accountWiz, account):
158
accountWiz.child(label="Full Name:").text = account.fullName
159
accountWiz.child(label="Email Address:").text = account.emailAddress
160
accountWiz.child(label="Reply-To:").text = account.replyTo
161
accountWiz.child(label="Organization:").text = account.organisation
162
accountWiz.clickForward()
164
def __doReceivingEmailPage(self, accountWiz, account):
165
# "Receiving Email" page:
166
accountWiz.child(label="Server Type: ").combovalue=account.getReceivingComboValue()
168
if isinstance(account, ExchangeAccount):
169
if self.evoVersion>Version([2,3,0]):
170
accountWiz.child(label="Username:").text = account.windowsUsername
171
accountWiz.child(label="OWA Url:").text = account.urlForOWA
173
# For Exchange, in Evolution 2.3 and later we have to click on the authenticate button to get further:
174
accountWiz.child("Authenticate").click()
175
self.doPasswordDialog(account.password, True)
176
self.detectAuthFailure()
178
accountWiz.child(label="Exchange Server:").text = account.server
179
accountWiz.child(label="Windows Username:").text = account.windowsUsername
181
elif isinstance(account,MixedAccount):
182
# In Evolution 2.0.4, server field is labelled "Host:", in Evolution 2.4.0 it is labelled "Server:"
183
# FIXME: check for other versions of Evolution; what is the _exact_ test here?
184
if self.evoVersion<Version([2,4,0]):
185
accountWiz.child(label="Host:").text = account.receiveMethod.server
187
accountWiz.child(label="Server:").text = account.receiveMethod.server
188
accountWiz.child(label="Username:").text = account.receiveMethod.username
189
# FIXME: "Use secure Connection"
190
# FIXME: "Check for Supported Types"
191
# FIXME: "Remember password"
193
raise NotImplementedError
195
accountWiz.clickForward()
197
def __doReceivingOptionsPage(self, accountWiz, account):
198
# "Receiving Options" page:
200
accountWiz.clickForward()
202
def __doSendingEmailPage(self, accountWiz, account):
203
# "Sending Email" page:
204
# FIXME: this fails; it picks up on the "Server Type:" from the earlier page
205
# and hence can't find the value it wants in the combo. Should use the correct page for all of this...
206
sendingEmailPage = accountWiz.currentPage()
207
sendingEmailPage.child(label="Server Type: ").combovalue = account.getSendingComboValue()
208
print sendingEmailPage
209
if isinstance(account,MixedAccount):
210
if isinstance(account.sendMethod,SMTPSettings):
211
sendingEmailPage.child(label="Server:").text = account.sendMethod.server
212
# FIXME: "Server requires authentication"
215
# FIXME: is this visible? "SSL is not supported in this build of Evolution"
216
elif isinstance(account, ExchangeAccount):
217
# Nothing should need doing
220
raise NotImplementedError
221
accountWiz.clickForward()
223
def __doAccountManagementPage(self, accountWiz, accountName):
224
# "Account Management" page:
225
accountWiz.child(label="Name:").text = accountName
226
accountWiz.clickForward()
228
def createAccount(self, account, accountName):
230
Add a new account, running the Evolution Account Assistant, filling in
234
self.getConfigMenuItem().click()
235
settingsDlg = self.child("Evolution Settings", recursive=False)
236
#pageTabs = settingsDlg.child(roleName="page tab list")
237
# page tabs don't seem to be labelled in the tree... how do we get at this?
239
# for now, assume Mail Accounts tab is selected..
240
# Account wizard takes a while to appear...
241
settingsDlg.child("Add").click()
242
# this one needs a watch, I guess
244
accountWiz = Wizard(self.child("Evolution Account Assistant", recursive=False),"Evolution Account Assistant")
245
accountWiz.clickForward()
247
self.__doIdentityPage(accountWiz, account)
248
self.__doReceivingEmailPage(accountWiz, account)
249
self.__doReceivingOptionsPage(accountWiz, account)
250
self.__doSendingEmailPage(accountWiz, account)
251
self.__doAccountManagementPage(accountWiz, accountName)
254
accountWiz.child("Apply").click()
256
# FIXME: we should add a review stage where we check that all widgets have the correct settings. (But why should that even be necessary? Can our framework implement that on the script's behalf? perhaps with a UITransaction class or somesuch?)
258
def doFirstTimeWizard(self, account, accountName, timezoneName):
259
setupWiz = Wizard(self.window('Evolution Setup Assistant'))
260
setupWiz.clickForward()
262
self.__doIdentityPage(setupWiz, account)
263
self.__doReceivingEmailPage(setupWiz, account)
264
self.__doReceivingOptionsPage(setupWiz, account)
265
self.__doSendingEmailPage(setupWiz, account)
266
self.__doAccountManagementPage(setupWiz, accountName)
269
# FIXME: timezone selection doesn't yet work
270
#self.child("TimeZone Combobox").child(timezoneName).click()
271
setupWiz.clickForward()
274
setupWiz.child("Apply").click()
276
def composeEmail(self):
278
Utility function to start composing a new email.
280
Returns a Composer instance wrapping the new email composer window.
282
self.menu("File").child("Mail Message").click()
283
composer = self.window("Compose a message")
284
return Composer(composer)
286
def createMeeting(self):
288
Utility function to start creating a new meeting.
290
Returns a MeetingWindow instance wrapping the new meeting window.
292
self.menu("File").child("Meeting").click()
293
meetingWin = self.dialog("Meeting - No summary")
294
return MeetingWindow(meetingWin)
297
class Composer(Window):
299
Subclass of Window wrapping an Evolution email composer, with utility functions
302
def __init__(self, node):
303
Window.__init__(self, node)
305
#FIXME: doesn't seem to be accessible on FC3 with evolution-2.0.4/gtkhtml3-3.3.2
306
gtkhtmlPanel = self.child(name="Panel containing HTML", roleName="panel")
307
gtkhtmlPanel.debugName="GTKHtml panel"
308
self.htmlNode = gtkhtmlPanel.child(roleName="text")
310
def __setattr__(self, name, value):
312
# Set the To: ("addressee" if you're feeling fancy) of the email:
313
self.textentry("To:").text = value
314
elif name=="subject":
315
# Set the subject of the email:
316
self.child(label="Subject:").text = value
318
# Set the body text of the email:
319
self.htmlNode.text = value
321
# otherwise, use normal Python attribute-handling:
322
self.__dict__[name]=value
324
def setHtml(self, htmlFlag):
326
raise NotImplementedError
329
formatMenu = self.menu("Format")
330
formatMenu.menuItem("HTML").click()
332
def setHeader(self, level):
334
Set the text type to 'Heading 1-6'
336
formatMenu = self.menu("Format")
337
headingMenu = formatMenu.menu("Heading")
338
headingMenu.menuItem("Header %s"% level).click()
340
def setBulletedList(self):
342
Sets the text style to be 'Bulleted List'
344
formatMenu = self.menu("Format")
345
headingMenu = formatMenu.menu("Heading")
346
headingMenu.menuItem("Bulleted List").click()
348
def typeText(self, text):
349
self.htmlNode.typeText(text)
353
Clicks on the Send menu item to send this email. Use with caution!
355
self.menuItem("Send").click()
357
def testUndoRedo(self):
359
Repeatedly does an 'Undo' until everything is undone, then does a 'Redo'
360
that many times, and checks that the content is the same.
362
Unfortunately, this doesn't work; Undo is always sensitive, even when
363
there's nothing to undo:
364
http://bugzilla.gnome.org/show_bug.cgi?id=257214
367
undo = self.menuItem("Undo")
368
redo = self.menuItem("Redo")
370
originalText = self.htmlNode.text
372
while undo.sensitive:
377
for i in range(numUndos):
380
newText = self.htmlNode.text
382
if newText!=originalText:
383
raise UndoRedoException(originalText, newText)
385
class UndoRedoException(Exception):
386
def __init__(self, originalValue, newValue):
387
self.originalValue = originalValue
388
self.newValue = newValue
391
return "Original value %s not equal to new value %s"%(originalValue, newValue)
393
class AppointmentWindow(Window):
395
Subclass of Window wrapping an Evolution appointment window, with utility functions
397
def __init__(self, node):
398
Window.__init__(self, node)
400
def __setattr__(self, name, value):
402
self.tab("Appointment").child(label="Summary:").text = value
404
# otherwise, use normal Python attribute-handling:
405
self.__dict__[name]=value
407
class MeetingWindow(AppointmentWindow):
409
Subclass of AppointmentWindow wrapping an Evolution meeting window, with utility functions
411
def __init__(self, node):
412
AppointmentWindow.__init__(self, node)
413
self.invitationsTab = self.tab("Invitations")
414
self.attendeeTable = self.invitationsTab.child(roleName='table')
416
def addAttendee(self, attendee, attendeeType, role, rsvp, status):
418
Add an attendeee to this meeting. Doesn't fully work yet.
420
self.invitationsTab.button('Add').click()
422
class SelectFolderDialog(Window):
424
Subclass of Window wrapping an Evolution 'Select Folder' dialog, with
428
def __init__(self, node, evoApp):
429
Window.__init__(self, node)
432
def newInboxSubfolder(self, name):
433
self.child("New").click()
434
createFolderDlg = self.evoApp.dialog("Create folder")
435
createFolderDlg.child(label="Folder name:").text = name
437
tree = self.child("Mail Folder Tree")
438
tableCell = "On This Computer"
440
# FIXME: need implement table stuff for this...
441
#"On This Computer" / "Inbox"
443
# FIXME: select parent folder
444
createFolderDlg.child("Create").click()
445
# this is only clickable if we have a sane folder name and a selected parent
447
# FIXME: now need to select the new folder in the Select Folder dialog
449
# FIXME: finally, should click on OK (or should the caller do this?)
453
Base class for an Evolution account, for use when creating test accounts
455
def __init__(self, fullName, emailAddress, replyTo="", organisation="", password=""):
456
self.fullName = fullName
457
self.emailAddress = emailAddress
458
self.replyTo = replyTo
459
self.organisation = organisation
460
self.password = password
462
def getReceivingComboValue(self):
463
raise NotImplementedError
465
def getSendingComboValue(self):
466
raise NotImplementedError
468
class UseSecureConnection:
469
NEVER, WHENEVER, ALWAYS = ("NEVER", "WHENEVER", "ALWAYS")
471
class ExchangeAccount(Account):
473
Subclass of Account representing an Exchange account
475
def __init__(self, fullName, emailAddress, windowsUsername, server="", urlForOWA="", replyTo="", organisation="", password=""):
476
Account.__init__(self, fullName, emailAddress, replyTo, organisation, password)
477
self.windowsUsername = windowsUsername
479
self.urlForOWA = urlForOWA
481
def getReceivingComboValue(self):
482
return "Microsoft Exchange"
484
def getSendingComboValue(self):
485
return "Microsoft Exchange"
487
class MixedAccount(Account):
489
Subclass of Account representing a combination of a receiving method and a
492
def __init__(self, fullName, emailAddress, receiveMethod, sendMethod, replyTo="", organisation="", ):
493
Account.__init__(self, fullName, emailAddress, replyTo, organisation, password="")
494
assert isinstance(receiveMethod, ReceiveSettings)
495
assert isinstance(sendMethod, SendSettings)
496
self.receiveMethod = receiveMethod
497
self.sendMethod = sendMethod
499
def getReceivingComboValue(self):
500
return self.receiveMethod.getServerTypeComboValue()
502
def getSendingComboValue(self):
503
return self.sendMethod.getServerTypeComboValue()
505
class ReceiveSettings:
506
def getServerTypeComboValue(self):
507
raise NotImplementedError
510
def getServerTypeComboValue(self):
511
raise NotImplementedError
513
class IMAPSettings(ReceiveSettings):
514
def __init__(self, server, username, useSecureConnection, authenticationType, rememberPassword=False):
516
self.username = username
517
self.useSecureConnection = useSecureConnection
518
self.authenticationType = authenticationType
519
self.rememberPassword = rememberPassword
521
def getServerTypeComboValue(self):
524
class SMTPSettings(SendSettings):
525
def __init__(self, server, useSecureConnection, authenticationType="plain", requireAuth=False, username="", rememberPassword=False):
527
self.username = username
528
self.useSecureConnection = useSecureConnection
529
self.authenticationType = authenticationType
530
self.rememberPassword = rememberPassword
531
self.requireAuth = requireAuth
533
def getServerTypeComboValue(self):
536
class SendmailSettings(SendSettings):
537
def getServerTypeComboValue(self):
540
class ReceiptOptions:
542
Class containing the options found on the 'Receiving Email' page of the
543
configuration dialogs.
545
def __init__(self, autocheckOn=False, autocheckInterval=10, customConnectionCommand=None, showOnlySubscribedFolders=False, overrideNamespace=None, applyFiltersToNewInInbox=False, checkNewForJunk=False, onlyCheckForJunkInInbox=False, autosyncRemoteMailLocally=False):
546
raise NotImplementedError
549
focus.application("evolution")
552
focus.application("evolution")
553
focus.dialog("Evolution Import Assistant")
554
click("Forward") # grrr... selects the Forward menu item, rather than the Forward button
556
def blowAwayEvolution():
558
Helper function for testing Evolution.
562
This forcibly shuts down evolution, then blows away all Evolution settings.
563
(FIXME: first copy the GConf data first to ./evolution-gconf-backup.xml ???)
565
os.system ('evolution --force-shutdown')
567
os.system ('gconftool-2 --recursive-unset /apps/evolution')
570
def doFirstTimeWizard(account, accountName, timezoneName):
572
Helper function for testing Evolution.
576
This forcibly shuts down evolution, then blows away all Evolution settings.
577
(FIXME: first copy the GConf data first to ./evolution-gconf-backup.xml ???)
579
It then runs the first time wizard with the given settings.
581
Returns an EvolutionApp instance.
587
evo.doFirstTimeWizard(account, accountName, timezoneName)
591
def setBogusGConfAccountData():
593
Helper function for testing Evolution.
597
This writes totally bogus account data into the GConf key
598
"/apps/evolution/mail/accounts", in order to hack past the first-time
601
os.system('gconftool-2 --type list --list-type=string --set /apps/evolution/mail/accounts ["This is bogus data that is not even an attempt at well-formed XML"]')