Index: /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_browser.rst
===================================================================
--- /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_browser.rst	(revision 1219)
+++ /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_browser.rst	(revision 1219)
@@ -0,0 +1,109 @@
+Quills browser tests
+====================
+
+N.B. Much of the generic browser testing has been factored out into
+quills/app/tests/browser.rst.  *Only* browser tests that are *specific* to the
+Products.QuillsEnabled implementation (as opposed to Products.Quills) should be
+in this file.
+
+
+Here we click ourselves through a Quills instance and check that everything is
+in order. First some boilerplate to get our browser up and running:
+
+    >>> self.setRoles(("Contributor",))
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = False
+
+
+adding content
+**************
+
+    >>> browser.open('http://nohost/plone/weblog/')
+    >>> browser.getLink(url='http://nohost/plone/weblog/createObject?type_name=Document').click()
+    >>> browser.getControl('Title').value = "New entry"
+    >>> browser.getControl('Description').value = "A new entry"
+    >>> browser.getControl('Text').value = "This is a new entry."
+    >>> browser.getControl(name='allowDiscussion:boolean').value = True
+    
+    We'd like to set the subjects, but they are on a different 'tab' for
+    standard 'Document'/'Page' types in plone, so we skip it for now.
+    
+    #>>> browser.getControl(name='subject_existing_keywords:list').value = ["fishslapping",]
+    >>> browser.getControl('Save').click()
+
+Having filled out the form and saved it we should now be viewing our newly baked
+entry:
+
+    >>> browser.url
+    'http://nohost/plone/weblog/new-entry'
+
+However, since we only have the Contributor role, we are not allowed to publish
+the item:
+
+    >>> browser.getLink('Publish')
+    Traceback (most recent call last):
+    ...
+    LinkNotFoundError
+
+But we can submit the entry for publication:
+
+    >>> browser.getLink('Submit').click()
+
+If we additionally grant the `Reviewer` role we can publish the new entry:
+
+    >>> self.setRoles(("Contributor", "Reviewer"))
+    >>> browser.reload()
+    >>> browser.getLink('Publish').click()
+
+
+comments
+********
+
+By default we even need the `Manager` role to add comments:
+
+    >>> self.setRoles(("Contributor", "Reviewer", "Manager"))
+
+We also need to enable comments for the 'Document' portal type:
+
+    >>> dtype = self.portal.portal_types['Document']
+    >>> dtype.manage_changeProperties(allow_discussion=1)
+    >>> entry_content = self.weblog.context['new-entry']
+    >>> entry_content.allowDiscussion = 1
+    >>> entry_content.reindexObject()
+
+Now we open the page in a browser:
+
+    >>> browser.open('http://nohost/plone/weblog/new-entry')
+
+The non-archive view has a button to add a comment:
+
+#XXX: quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+    >>> browser.getControl('Subject')
+    <Control name='subject' type='text'>
+
+When viewing an entry via its archive url, we still should be able to add a
+comment, as well:
+
+    >>> entry = self.weblog.getEntry('new-entry')
+    >>> date = entry.getPublicationDate()
+    >>> year = str(date.year())
+    >>> month = str(date.month()).zfill(2)
+    >>> day = str(date.day()).zfill(2)
+
+    >>> browser.open('http://nohost/plone/weblog/%s/%s/%s/new-entry' % (year, month, day))
+
+#XXX: quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+    >>> browser.getControl('Subject')
+    <Control name='subject' type='text'>
+
+    >>> browser.getControl('Subject').value = "Parrot"
+    >>> browser.getControl('Comment').value = "Is dead. Is deceased."
+
+However, currently this still raises an error (eventhough the comment is
+actually created). The problem seems to be that zope.testbrowser cannot handle
+urls that have anchors (e.g. 'foo/bar#anchor') at the end. The comment machinery
+tries to redirect to such a url after the 'click', so this next call fails. See
+issue http://plone.org/products/quills/issues/105 for more details of why this
+test exists.
+
+    >> browser.getControl('Save').click()
Index: /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_fixedBugs.rst
===================================================================
--- /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_fixedBugs.rst	(revision 1219)
+++ /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_fixedBugs.rst	(revision 1219)
@@ -0,0 +1,1018 @@
+Quills browser tests
+====================
+
+Here we check for fixed bugs using tests, that don't fit into the 'narrative' in
+the main browser test. First some boilerplate to get our browser up and running:
+
+    >>> self.setRoles(("Contributor",))
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = False
+    >>> entry = self.weblog.addEntry("Blog entry",
+    ...                              "Just for testing",
+    ...                              "Nothing to see.",
+    ...                              ['fishslapping'],
+    ...                              id="entry")
+    >>> from quills.core.interfaces import IWeblogEntry
+    >>> IWeblogEntry.providedBy(entry)
+    True
+
+Make it discussable and publish it.
+Original quills.app tests use standard Add comment functionality, but 
+quintagroup.quills.extras package use quintagroup.plonecomments package with
+changed commenting UI. That is because I comment some lines, related to 
+commenting UI.
+
+    >>> entry = self.weblog.getEntry('entry')
+    >>> entry_content = entry.getWeblogEntryContentObject()
+    >>> entry_content.allowDiscussion(allowDiscussion=True)
+    >>> entry.publish()
+
+    >>> date = entry.getPublicationDate()
+    >>> year = str(date.year())
+    >>> month = str(date.month()).zfill(2)
+    >>> day = str(date.day()).zfill(2)
+
+    >>> self.setRoles(("Contributor", "Reviewer", "Manager"))
+    >>> browser.open('http://nohost/plone/weblog/%s/%s/%s/entry' 
+    ...              % (year, month, day))
+
+#    >>> browser.getControl('Add Comment').click()
+
+    >>> browser.getControl('Subject').value = "Parrot"
+    >>> browser.getControl('Comment').value = "Is dead. Is deceased."
+
+Issue #111 shows that the URLs generated by the archive portlet are not correct.
+Even when the weblog is not supposed to be using an extra 'archive' URL segment,
+the URLs always have that segment in them.
+
+To test this, we'll first make sure that the weblog config is setup to use the
+'archive' segment for URLs.
+
+    >>> from quills.core.interfaces import IWeblogConfiguration
+    >>> config = IWeblogConfiguration(self.weblog.getWeblogContentObject())
+    >>> config.archive_format = 'archive'
+
+Now we'll get a page and check its body for the appropriate link.
+
+    >>> browser.open('http://nohost/plone/weblog/')
+    >>> url = '<a href="http://nohost/plone/weblog/archive/%s/%s">'
+    >>> url = url % (year, month)
+    >>> url in browser.contents
+    True
+
+Now, if we change the archive_format, we should get different URLs.
+
+    >>> config.archive_format = ''
+    >>> browser.open('http://nohost/plone/weblog/')
+    >>> url = '<a href="http://nohost/plone/weblog/%s/%s">'
+    >>> url = url % (year, month)
+    >>> url in browser.contents
+    True
+
+
+There was an issue whereby the correct comment count didn't get shown for each
+weblog entry displayed in the weblog_view.  We verify that this is no longer
+the case here.
+
+First, let's add a comment so that we know one is there.
+
+    >>> from Products.CMFCore.utils import getToolByName
+    >>> dtool = getToolByName(self.portal, 'portal_discussion')
+    >>> entry_discussion = dtool.getDiscussionFor(entry_content)
+    >>> comment_id = entry_discussion.createReply(title='Comment Title',
+    ...                                           text='a little test body')
+
+Now, when we look at the weblog view, we should find that there is a link to the
+comments for `entry', together with a count of how many comments there are on
+it.
+
+    >>> browser.open('http://nohost/plone/weblog/')
+    >>> url = '<a href="http://nohost/plone/weblog/%s/%s/%s/entry#comments"'
+    >>> url = url % (year, month, day)
+    >>> url in browser.contents
+    True
+    >>> '<span>1</span>' in browser.contents
+    True
+
+>> repr(browser.contents)
+
+This last line of test is fairly lame as it could potentially match anywhere in
+the source.  An example of what we are really trying to match is the following:
+
+"""
+          <a href="http://nohost/plone/weblog/2007/09/24/entry#comments"
+           style="text-decoration: none;">
+          Comments:
+          </a>
+
+          <span>3</span>
+"""
+
+
+Issue #112 found that the recent comments portlet was generating incorrect links
+to comments as it wasn't utilising the archive URL of the weblog entry objects.
+
+    >>> txt = '<a href="http://nohost/plone/weblog/%s/%s/%s/entry#%s"'
+    >>> txt = txt % (year, month, day, comment_id)
+    >>> txt in browser.contents
+    True
+
+
+Issue #117 found that the weblog admin portlet got displayed to anonymous users,
+rather than being restricted to admin-ish users.  Let's verify that this is no
+longer the case.
+
+    >>> self.setRoles([])
+    >>> browser = self.getBrowser(logged_in=False)
+    >>> browser.handleErrors = False
+    >>> browser.open('http://nohost/plone/weblog/')
+    >>> 'portletWeblogAdmin' in browser.contents
+    False
+
+
+Issue #143: Portlets do not show up in empty blogs
+--------------------------------------------------
+
+This issue is caused by the way BasePortletRenderer implements ``available``.
+
+We do not use one of Quills' portlet here but make our own, as the problem
+is located in BasePortletRenderer.
+
+    >>> from plone.app.portlets.portlets import base
+    >>> from quills.app.portlets.base import BasePortletRenderer
+    >>> class TestRenderer(BasePortletRenderer, base.Renderer):
+    ...     """A simple Renderer"""
+    ...     pass
+
+Now create a blog. And see if we can get our portlet renderer. We first try
+with an empty blog. This a bit overly complicated because this test must work
+with both Quills and QuillsEnabled.
+
+    >>> blog = self.createBlog('issue-143')
+    >>> blogFolder = self.portal['issue-143']
+    >>> from zope.component import getMultiAdapter
+    >>> request = blogFolder.REQUEST
+
+The request will normally be marked by the traversal code to show we are inside
+a weblog. We have to do it here ourselves.
+
+    >>> from zope.interface import alsoProvides
+    >>> from quills.app.traversal import IInsideWeblog
+    >>> alsoProvides(request, IInsideWeblog)
+
+    >>> view = getMultiAdapter((blogFolder, request), name='view')
+    >>> renderer = TestRenderer(blogFolder, request, view, None, None)
+    >>> renderer.available
+    True
+
+Now with one private entry in it.
+
+    >>> entry = blog.addEntry('Tesing issue #143', 'Nothing', 'Nothing',
+    ...	                      id="issue-143")
+    >>> renderer.available
+    True
+
+And now with that one published. In all three cases the portlet should show up.
+We cannot do this directly on entry as it might be only an adapter.
+
+    >>> from Products.CMFCore.utils import getToolByName
+    >>> wft = getToolByName(self.getPortal(), 'portal_workflow')
+    >>> wft.getInfoFor(blogFolder['issue-143'], 'review_state')
+    'private'
+
+    >>> entry.publish()
+    >>> renderer.available
+    True
+
+
+Issue #115: Blog posts published in the future should not appear
+----------------------------------------------------------------
+
+This was not a bug, really. Quills behave correctly, hiding entries scheduled
+for future publication as it should. This test-case confirms this.
+
+We will test here access by Quills API and through the web.
+
+    >>> from quills.core.interfaces.weblog import IWeblog
+    >>> blog = self.weblog
+    >>> IWeblog.providedBy(blog)
+    True
+
+We create a entry and publish, though not yet in the future.
+    
+
+    >>> id = 'issue-115'
+    >>> entry = blog.addEntry("Issue #115", "Tesing for issue 115",
+    ...                       "Nothing.", id=id)
+    >>> entry.publish()
+
+This entry should have an effective date before now, or none at best. We cannot
+get effective directly from the entry because it might be only an adapter.
+
+    >>> from DateTime import DateTime # We cannot use python datetime here, alas
+    >>> effective = self.portal.weblog[id].effective()
+    >>> now = DateTime()
+    >>> effective is None or effective <= now
+    True
+    
+It is visible.
+
+    >>> id in map(lambda x: x.id, blog.getEntries())
+    True
+
+Now make it become effective in the future. It should still be visible since
+we are managers and possess the appropriate rights.
+
+    >>> from Products.CMFCore.permissions import AccessInactivePortalContent
+    >>> from Products.CMFCore.utils import _checkPermission
+    >>> _checkPermission(AccessInactivePortalContent,
+    ...		 self.portal.weblog) and True
+    True
+
+    >>> futureDate = now + 7
+    >>> self.portal.weblog[id].setEffectiveDate(futureDate)
+    >>> self.portal.weblog[id].indexObject()
+    >>> id in map(lambda x: x.id, blog.getEntries())
+    True
+        
+Now we drop that right. The entry should no longer be visible.
+
+    >>> from AccessControl import getSecurityManager
+    >>> self.logout()
+    >>> _checkPermission(AccessInactivePortalContent,
+    ...                  self.portal.weblog) and True
+        
+    >>> id in map(lambda x: x.id, blog.getEntries())
+    False
+
+If published in the past it should be visible again.
+    
+    >>> self.portal.weblog[id].setEffectiveDate(effective)
+    >>> self.portal.weblog[id].indexObject()
+    >>> id in map(lambda x: x.id, blog.getEntries())
+    True
+    
+Login again and set for future publication.
+
+    >>> self.loginAsPortalOwner()
+    >>> _checkPermission(AccessInactivePortalContent,
+    ...	                 self.portal.weblog) and True
+    True
+    >>> self.portal.weblog[id].setEffectiveDate(futureDate)
+    >>> self.portal.weblog[id].indexObject()
+    >>> id in map(lambda x: x.id, blog.getEntries())
+    True
+
+Now same procedure through the web. Our entry should be invisible.
+
+    >>> browser = self.getBrowser(logged_in=False)
+    >>> browser.handleErrors = False
+    >>> browser.open('http://nohost/plone/weblog/')
+    >>> browser.getLink(url="http://nohost/plone/weblog/%s" % (id,))
+    Traceback (most recent call last):
+        ...
+    LinkNotFoundError
+
+After resetting the date it should be visible again.
+
+    >>> self.portal.weblog[id].setEffectiveDate(effective)
+    >>> self.portal.weblog[id].indexObject()
+    >>> browser.open('http://nohost/plone/weblog/')
+    >>> browser.getLink(url="http://nohost/plone/weblog/%s" % (id,))
+    <Link ...>
+
+We do not test for draft stated entries, because those are hidden from public
+viewing anyway. We have to check the archive, though.
+
+First some preparations, like getting the archive URL prefix.
+
+    >>> from quills.app.interfaces import IWeblogEnhancedConfiguration
+    >>> weblog_config = IWeblogEnhancedConfiguration(self.portal.weblog)
+    >>> archivePrefix = weblog_config.archive_format
+
+We check through the web only. First with effective in the past.
+
+    >>> path = "/".join([archivePrefix, "%s" % (effective.year(),)])
+    >>> browser.open('http://nohost/plone/weblog/%s' % (path,))
+    >>> browser.getLink(url="http://nohost/plone/weblog/%s" % (id,))
+    <Link ...>
+
+Then with effective in the future.
+
+    >>> path = "/".join([archivePrefix, "%s" % (futureDate.year(),)])
+    >>> self.portal.weblog[id].setEffectiveDate(futureDate)
+    >>> self.portal.weblog[id].indexObject()
+    >>> browser.open('http://nohost/plone/weblog/%s' % (path,))
+    >>> browser.getLink(url="http://nohost/plone/weblog/%s" % (id,))
+    Traceback (most recent call last):
+        ...
+    LinkNotFoundError
+
+Finally we should test syndication, but this would require some package
+implementing that feature, which we do not want do depend on here.
+
+
+Issue #158 — "Add Entry" of the Weblog Admin portlet fails
+-----------------------------------------------------------
+
+An exception is raised that, because the specified portal type does not exist.
+In fact the type specified is "None". This is happens because no default
+type is configured for Products.Quills weblogs.
+
+XXX: Test-case does not work for QuillsEnabled!
+
+Create a fresh blog, in the case someone might accidentally have set a default
+portal type before. Populate it a little.
+
+    >>> self.setRoles(("Manager",))
+    >>> blog = self.createBlog('issue-158')
+    >>> blogFolder = self.portal['issue-158']
+    >>> entry = blog.addEntry('Tesing issue #158', 'Nothing',
+    ...                       'Nothing', id="issue-158")
+    >>> entry.publish()
+
+Now click the "Add Entry" link. The edit form should be present.
+
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = True
+    >>> browser.open('http://nohost/plone/issue-158/')
+    >>> browser.getLink(text='Add Entry').click()
+    >>> '/portal_factory/' in browser.url
+    True
+
+
+Issues #149 & #162: Memory leak and folder listing breakage
+-----------------------------------------------------------
+
+Both issues are cause by the way Quills wraps up Catalog Brains into an
+IWeblogEntry adapter. It sets this wrapper class with "useBrains" of
+Products.ZCatalog.Catalog. Doing so on each query causes the memory leak, as
+the Catalog creates a class on the fly around the class passed to useBrains.
+Never resetting the class causes the folder listing to break, because now
+all catalog queries, even those from non Quills code, use Quills custom Brain.
+This brain however defines methods which are simple member variable in the
+default Brain, causing those clients to break.
+
+To test for those bug, first publish a post, then render the Weblog View once.
+This will cause some of the incriminating code to be called. Testing all 
+occurances would not be sensible. A fix must make sure to break all those
+calls by renaming the custom catalog class!
+
+An exception is raised that, because the specified portal type does not exist.
+In fact the type specified is "None". This is happens because no default
+type is configured for Products.Quills weblogs.
+Create a fresh blog, in the case someone might accidentally have set a default
+portal type before. Populate it a little.
+
+    >>> entry = self.weblog.addEntry('Tesing issue # 149 & #162', 'Nothing',
+    ...                       'Nothing', id="issue-158")
+    >>> entry.publish()
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = True
+    >>> browser.open('http://nohost/plone/weblog/')
+
+Now query a non Quills object from the catalog (in fact no query should ever
+return a custom Quills brain). At least the Welcome message should exist.
+Then check if the brain is a Quills adapter.
+
+    >>> from Products.CMFCore.utils import getToolByName
+    >>> catalog = getToolByName(self.portal, 'portal_catalog')
+    >>> results = catalog(path="/", portal_type="Document")
+    >>> len(results) > 0
+    True
+
+    >>> from quills.core.interfaces import IWeblogEntry
+    >>> IWeblogEntry.providedBy(results[0])
+    False
+
+    
+Issue #172 — Can't log comments from default view on weblog entries
+-------------------------------------------------------------------
+
+Quills default view for Weblog Entries is named 'weblogentry_view'. Plone
+however links to individual items via the 'view' alias. This happens for
+instance in collections or the recent items portlet. The Weblog Entries
+still get rendered, important actions are missing though, e.g. the user
+actions for copy/paste/delete or workflow actions. The commenting button 
+is also missing.
+
+We will need write access to the blog.
+
+    >>> self.logout()
+    >>> self.login()
+    >>> self.setRoles(("Manager",))
+
+Create a discussable weblog entry first.
+
+    >>> from quills.app.browser.weblogview import WeblogEntryView
+    >>> traverseTo = self.portal.restrictedTraverse # for brevity
+    >>> entry = self.weblog.addEntry("Test for issue #172", "Nothing",
+    ...                              "Nothing", id="issue-172")
+    >>> entry_content = entry.getWeblogEntryContentObject()
+    >>> entry_content.allowDiscussion(allowDiscussion=True)
+    >>> entry.publish()
+
+There should be a fully functionaly WeblogEntryView at 'weblogentry_view'.
+
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = False
+    >>> browser.open('http://nohost/plone/weblog/issue-172/weblogentry_view')
+
+That inculdes actions like cut and paste,
+
+    >>> browser.getLink(text='Actions') # of issue-172/weblogentry_view
+    <Link ...>
+
+and also workflow control,
+
+    >>> browser.getLink(text='State:') # of issue-172/weblogentry_view
+    <Link ...>
+
+#XXX quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+
+#    >>> browser.getForm(name='reply') # of issue-172/weblogentry_view
+#    <zope.testbrowser.browser.Form object at ...>
+    >>> browser.getControl('Subject')
+    <Control name='subject' type='text'>
+
+and finally commenting, which must be enabled, of course.
+
+The same should be available when we navigate to issue-172/view.
+
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = False
+    >>> browser.open('http://nohost/plone/weblog/issue-172/view')
+
+That inculdes actions like cut and paste,
+
+    >>> browser.getLink(text='Actions') # of issue-172/view
+    <Link ...>
+
+and also workflow control,
+
+    >>> browser.getLink(text='State:') # of issue-172/view
+    <Link ...>
+
+and finally commenting, which must be enabled, of course.
+
+#XXX quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+
+#    >>> browser.getForm(name='reply') # of issue-172/weblogentry_view
+#    <zope.testbrowser.browser.Form object at ...>
+    >>> browser.getControl('Subject')
+    <Control name='subject' type='text'>
+
+
+Issue #180: Incorrect author links in bylines
+---------------------------------------------
+
+Our screen name must differ from the login name to make this issue apparent.
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+    >>> from Products.CMFCore.utils import getToolByName
+    >>> pmtool = getToolByName(self.portal, 'portal_membership')
+    >>> iAm = pmtool.getAuthenticatedMember()
+    >>> myId  = iAm.getId()
+    >>> oldName = iAm.getProperty('fullname')
+    >>> newName = "User Issue180"
+    >>> iAm.setProperties({'fullname': newName})
+
+We need to add a page. Usually we would do so as a Contributor, but publishing
+the entry without approval requires the Manager role, too.
+
+    >>> entry = self.weblog.addEntry(title="Issue #180", id="issue180",
+    ...                      excerpt="None", text="None")
+    >>> entry.publish()
+    
+Now check the author links. First when showing the entry only.
+
+    >>> browser = self.getBrowser()
+    >>> browser.open("http://nohost/plone/weblog/issue180")
+    >>> link = browser.getLink(text=newName)
+    >>> link.url == "http://nohost/plone/weblog/authors/%s" % (myId,)
+    True
+
+Now the blog view.
+
+    >>> browser.open("http://nohost/plone/weblog")
+    >>> link = browser.getLink(text=newName)
+    >>> link.url == "http://nohost/plone/weblog/authors/%s" % (myId,)
+    True
+
+Reset user name.
+    
+    >>> iAm.setProperties({'fullname': oldName})
+
+
+Issue #119: Archive URL not respected when commenting a post
+------------------------------------------------------------
+
+When you add comment to a post, you will end up at the absolute URL of
+the post no matter if you came from the archive.
+
+To test this I will add a post and navigate to it by archive URL. The entry
+must be commentable. It will have a fixed publication date to easy testing.
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+
+    >>> entry = self.weblog.addEntry(title="Issue #119", id="issue119",
+    ...                      excerpt="None", text="None")
+    >>> entry_content = entry.getWeblogEntryContentObject()
+    >>> entry_content.allowDiscussion(allowDiscussion=True)
+    >>> entry.publish(pubdate=DateTime("2009-04-28T09:46:00"))
+
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.open("http://nohost/plone/weblog/2009/04/28/issue119")
+
+Now add a comment through the web. Saving it should take us back from where we
+from.
+
+Because of quintagroup.quills.extras uses quintagorup.plonecomments - UI for
+commenting slightly different from standard one. That is whay I change some
+testing code.
+
+    >>> browser.handleErrors = True
+
+#XXX quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+#    >>> browser.getControl("Add Comment").click()
+
+    >>> browser.getControl('Subject').value = "Issue 119"
+    >>> browser.getControl('Comment').value = "Redirect to archive, please!"
+
+Unfortunately, the clicking submit will cause a 404 error. At least up until
+Zope 2.10.6 zope.testbrowser and/or mechanize handle URL fragments incorrectly.
+They send them to the server (which they should) who then chokes on them.
+Recent versions of mechanize (?) and testbrowser (3.5.1) have fixed that. I
+cannot find out though which version of testbrowser ships with individual Zope
+releases. As soon as this is fixed the try-except-clause may safely go away.
+
+With Products.Quills this test-case will fail for another reason. There the
+redirect handler (quills.app.browser.discussionreply) is not registered during
+testing; probably because of the GS profile in the tests module. FIX ME!
+
+    >>> from urllib2 import HTTPError
+    >>> try:
+    ...     browser.getControl('Save').click()
+    ... except HTTPError, ex:
+    ...     if ex.code != 404:
+    ...         raise
+    >>> browser.url.split('#')[0]
+    'http://nohost/plone/weblog/2009/04/28/issue119'
+
+
+Issue #189: Replying to an comment raises a non-fatal TypeError
+---------------------------------------------------------------
+
+This issue was caused by Quills' portlets trying to locate the weblog object.
+They would try to adapt a DiscussionItem to IWeblogLocator. This would happen
+only for responses given, because comment on post have the weblog entry as
+context set.
+
+Btw. adding visiting Quills uploads or topic image folder or adding anything
+to them would raise the same error.
+
+To test for this issue we will add a comment and a reply and see whether
+our portlet show up in the reply form.
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+
+    >>> entry = self.weblog.addEntry(title="Issue #189", id="issue189",
+    ...                      excerpt="None", text="None")
+    >>> entry_content = entry.getWeblogEntryContentObject()
+    >>> entry_content.allowDiscussion(allowDiscussion=True)
+    >>> entry.publish( pubdate=DateTime("2009-04-28T16:48:00") )
+
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = True
+    >>> browser.open("http://nohost/plone/weblog/issue189")
+
+Add the comment to the post. See if there appears some text which indicates
+the presence of the Administration portlet.
+
+
+Because of quintagroup.quills.extras uses quintagorup.plonecomments - UI for
+commenting slightly different from standard one. That is whay I change some
+testing code.
+
+#XXX quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+#    >>> browser.getControl("Add Comment").click()
+
+    >>> browser.getControl('Subject').value = "Comment"
+    >>> browser.getControl('Comment').value = "This works"
+
+See test for issue #119 why this try-except statement is here.
+
+    >>> from urllib2 import HTTPError
+    >>> try:
+    ...     browser.getControl('Save').click()
+    ... except HTTPError, ex:
+    ...     if ex.code == 404:
+    ...         browser.open("http://nohost/plone/weblog/issue189")
+    ...     else:
+    ...         raise
+    >>> 'Weblog Admin' in browser.contents
+    True
+    
+Add a reply to that comment.
+      
+Because of quintagroup.quills.extras uses quintagorup.plonecomments - UI for
+commenting slightly different from standard one. That is whay I change some
+testing code.
+
+#XXX quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+And took into consideration #119
+
+    >>> try:
+    ...     browser.getControl("Publish").click()
+    ... except HTTPError, ex:
+    ...     if ex.code == 404:
+    ...         browser.open("http://nohost/plone/weblog/issue189")
+    ...     else:
+    ...         raise
+    >>> browser.getControl("Reply").click()
+    >>> 'Weblog Admin' in browser.contents
+    True
+
+
+Issue #194: Quills breaks commenting for non-weblog content
+-----------------------------------------------------------
+
+Adding a comment to non-Quills content, say a plain Document, would raise
+a NameError. This was caused by an undefined variable `redirect_target` in 
+`quills.app.browser.discussionreply`.
+
+To test for this issue we will add a comment to a Document outside the blog.
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+
+    >>> id = self.portal.invokeFactory("Document", id="issue194",
+    ...           title="Issue 179", description="A test case for issue #194.")
+    >>> self.portal[id].allowDiscussion(allowDiscussion=True)
+
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = True
+    >>> browser.open("http://nohost/plone/issue194")
+
+#XXX quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+#    >>> browser.getControl("Add Comment").click()
+
+    >>> browser.getControl('Subject').value = "Issue 194 fixed!"
+    >>> browser.getControl('Comment').value = "This works"
+
+See test for issue #119 why this try-except statement is here.
+
+    >>> from urllib2 import HTTPError
+    >>> try:
+    ...     browser.getControl('Save').click()
+    ... except HTTPError, ex:
+    ...     if ex.code == 404:
+    ...         browser.open("http://nohost/plone/issue194")
+    ...     else:
+    ...         raise
+    >>> 'Issue 194 fixed!' in browser.contents
+    True
+
+
+Issue #195: Topic view shows only one keyword
+---------------------------------------------
+
+This obviously only hurts when one tries to filter by more than just
+one keyword. Filtering by multiple keywords is done appending more
+keywords to the blog URL, separated by slashes.
+
+First blog a post in more than one catagory.
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+
+    >>> entry = self.weblog.addEntry(title="Issue #195", id="issue195",
+    ...                      topics=['i195TopicA', 'i195TopicB'],
+    ...                      excerpt="None", text="None")
+    >>> entry.publish()
+
+Now browse the weblog by those two keywords. They should appear in a
+heading.
+
+    >>> browser = self.getBrowser()
+    >>> browser.handleErrors = True
+    >>> browser.open("http://nohost/plone/weblog/topics/i195TopicA/i195TopicB")
+
+    >>> import re
+    >>> r1 = 'i195TopicA.+i195TopicB|i195TopicB.+i195TopicA'
+    >>> r2 = '<h1>(%s)</h1>' % (r1,)
+    >>> re.search(r2, browser.contents)
+    <_sre.SRE_Match object at ...>
+
+    >>> re.search(r1, browser.title)
+    <_sre.SRE_Match object at ...>
+
+Author topics face the same problem. So, the same with them. We need a second
+author first.
+
+    >>> from Products.CMFCore.utils import getToolByName
+    >>> aclUsers = getToolByName(self.getPortal(), 'acl_users')
+    >>> aclUsers.userFolderAddUser('Issue195Author2', 'issue195',['manager'],[])
+    >>> rawPost = self.portal.weblog['issue195']
+    >>> rawPost.setCreators(rawPost.Creators()+('Issue195Author2',))
+
+    >>> authors = rawPost.Creators()
+    >>> browser.open("http://nohost/plone/weblog/authors/%s/%s" % authors)
+
+    >>> import re
+    >>> from string import Template
+    >>> templ = Template('${a}.+${b}|${b}.+${a}')
+    >>> r1 = templ.substitute(a=authors[0], b=authors[1])
+    >>> r2 = '<h1>(%s)</h1>' % (r1,)  
+    >>> re.search(r2, browser.contents)
+    <_sre.SRE_Match object at ...>
+
+    >>> re.search(r1, browser.title)
+    <_sre.SRE_Match object at ...>
+
+
+Issue #198: Images disappear blog entry is viewed by Tag Cloud or Author Name
+-----------------------------------------------------------------------------
+
+The two topic views for authors and keyword would display an empty result page
+for keywords or author without associated posts. Surprisingly the archive view
+behaved correctly, most certainly because it is used more intensly and hence
+the bug could not go undetected there. Nonetheless we will test image handling
+for all three virtual containers here.
+
+Quills and QuillsEnabled handle image uploads differently. While a Quills blog
+contains a special folder for uploads, QuillsEnabled leaves folder organization
+to the user. Both however ought to be able to acquire content from containing
+locations. This is where we will put our test image.
+
+Image loading and creation is inspired by the test-cases ATContentTypes Image
+portal type and the test cases of quills.remoteblogging.
+
+    >>> import os
+    >>> import quills.app.tests as home
+    >>> path = os.path.dirname(home.__file__)
+    >>> file = open('%s/quills_powered.gif' % (path,), 'rb')
+    >>> imageBits = file.read()
+    >>> file.close()
+    
+    >>> id = self.portal.invokeFactory('Image', 'issue198.gif',
+    ...                                title="Image for Issue 198")
+    >>> image = self.portal[id]
+    >>> image.setImage(imageBits)
+
+Now we navigate to the image via the virtual URLs for archive, authors
+and topics. We log in as manager, because the image is private still.
+
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = False
+
+Before we start, let's try the canonical URL of the image.
+
+    >>> browser.open('http://nohost/plone/%s/view' % (id,))
+    >>> browser.title
+    '...Image for Issue 198...'
+
+We begin the archive. We create a post to make sure we actually have an
+archive. 
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+    >>> keyword = 'issue198kw' # id clashes would cause mayhem
+    >>> entry = self.weblog.addEntry(title="Issue #198", id="issue198",
+    ...                             topics=[keyword],
+    ...                             excerpt="None", text="None")
+    >>> entry.publish() 
+    >>> year = entry.getPublicationDate().year()
+    >>> month = entry.getPublicationDate().month()
+    >>> browser.open('http://nohost/plone/weblog/%s/%s/%s/view'
+    ...               % (year, month, id))
+    >>> browser.title
+    '...Image for Issue 198...'
+
+Now the author container. The bug caused a fat internal server error here, 
+which was in fact the ``TypeError: unsubscriptable object`` described
+in it's issue report.
+
+    >>> self.portal.error_log._ignored_exceptions = ()
+    >>> author = entry.getAuthors()[0].getId()
+    >>> browser.open('http://nohost/plone/weblog/authors/%s/view'
+    ...               % (id,))
+    >>> browser.title
+    '...Image for Issue 198...'
+
+Images and other acquired stuff may only appear directly after the name
+of the topic container (``authors`` here). Later names will be taken for
+keywords, no matter if they designated a picture somewhere. It simply would
+not make sense otherwise.
+
+    >>> browser.open('http://nohost/plone/weblog/authors/%s/%s/view'
+    ...               % (author,id))
+    >>> browser.title
+    'Posts by ...issue198.gif...'
+
+And finally the same for the keyword container.
+
+    >>> browser.open('http://nohost/plone/weblog/topics/%s/view'
+    ...               % (id,))
+    >>> browser.title
+    '...Image for Issue 198...'
+
+    >>> browser.open('http://nohost/plone/weblog/topics/%s/%s/view'
+    ...               % (keyword, id))
+    >>> browser.title
+    'Posts about ...issue198.gif...'
+
+
+
+Issue 202: Filtering by an non-existing author id causes a TypeError
+--------------------------------------------------------------------
+
+This was very much related to issue #198. Two scenarios cause this error
+actually, the one described in issue #198, and when a non existant author
+is queried. We simulate the latter here. It renders no blog entry.
+
+    >>> browser = self.getBrowser()
+    >>> browser.handleErrors = False
+    >>> browser.open('http://nohost/plone/weblog/authors/meNotThere202')
+    >>> browser.title
+    'Posts by meNotThere202...'
+
+    >>> browser.contents
+    '...No weblog entries have been posted...'
+
+On the other hand, querying an real *and* a fictive user name will render
+all posts of the reals user. This is due to "posts by any of the given
+authors" semantics of author topics.
+
+Before we check this, we post an entry.
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+    >>> entry = self.weblog.addEntry(title="Issue #202", id="issue202",
+    ...                             excerpt="None", text="None")
+    >>> entry.publish()
+
+Now, find out who we are.
+
+    >>> pmtool = getToolByName(self.portal, 'portal_membership')
+    >>> iAm = pmtool.getAuthenticatedMember()
+    >>> myId  = iAm.getId()
+
+And finally do the query.
+
+    >>> browser.open('http://nohost/plone/weblog/authors/meNotThere202/%s'
+    ...              % myId)
+    >>> browser.title
+    'Posts by meNotThere202...'
+    
+    >>> myId in browser.title
+    True
+
+    >>> browser.contents
+    '...<h2>...Issue #202...</h2>...'
+       
+
+Issue #203 — archive portlet broken: ValueError: invalid literal for int()
+---------------------------------------------------------------------------
+
+This bug was cause by quills.app.archive.BaseDateArchive.getId accidentally
+acquiring values for the attributes 'year', 'month' or 'day'. The product
+CalendarX unveiled this because it defines a page named 'day'. But in fact
+any property named 'day', 'month' or 'year' that might be acquired by
+climbing up the acquisition chain from an archive will cause this fault.
+
+To test this we will simply add three pages of those names just above the
+weblog. Then we will see, what the various archive report as their id.
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+    >>> portal = self.getPortal()
+
+We post an entry to be sure, that there is an archive.
+
+    >>> entry = self.weblog.addEntry(title="Issue #203", id="issue203",
+    ...                             excerpt="None", text="None")
+    >>> entry.publish() 
+
+No get the archives from year to day.
+
+    >>> aYearArchive = self.weblog.getSubArchives()[0]
+    >>> aMonthArchive = aYearArchive.getSubArchives()[0]
+    >>> aDayArchive = aMonthArchive.getSubArchives()[0]
+
+Create an potential acquisition target for attribute 'year' above the 
+blog. Then check if ``getId`` still reports numbers...
+
+    >>> portal.invokeFactory('Document', id='year', title='Year')
+    'year'
+    >>> type(int(aYearArchive.getId()))
+    <type 'int'>
+    >>> type(int(aMonthArchive.getId()))
+    <type 'int'>
+    >>> type(int(aDayArchive.getId()))
+    <type 'int'>
+
+Same for month.
+
+    >>> portal.invokeFactory('Document', id='month', title='Month')
+    'month'
+    >>> type(int(aYearArchive.getId()))
+    <type 'int'>
+    >>> type(int(aMonthArchive.getId()))
+    <type 'int'>
+    >>> type(int(aDayArchive.getId()))
+    <type 'int'>
+
+Same for day.
+
+    >>> portal.invokeFactory('Document', id='day', title='Day')
+    'day'
+    >>> type(int(aYearArchive.getId()))
+    <type 'int'>
+    >>> type(int(aMonthArchive.getId()))
+    <type 'int'>
+    >>> type(int(aDayArchive.getId()))
+    <type 'int'>
+
+Issue #204: Not Found when going to posts by archive URL
+--------------------------------------------------------
+
+This much the same as issue #203, only located elsewhere: this time
+time the traversal code. We simulate it here by simply going to any
+post in the archive.
+
+Do not move this test-case away from the one for issue #203, as it
+continues it! It depend on the pages created there.
+
+    >>> browser.open('http://nohost/plone/weblog')
+    >>> link = browser.getLink('Issue #203')
+    >>> link.click()
+    >>> browser.title
+    'Issue #203...'
+
+
+Issue #209 — UnicodeDecodeError in topics view
+----------------------------------------------
+
+Quills must allow non-ascii characters in topic names. This used to
+work but broke with a fix for issue #195 at r87933.
+
+We start as usual by post an entry, this time under a non-ascii
+topic.
+
+    >>> self.login()
+    >>> self.setRoles(('Manager',))
+    >>> keyword = 'issue198kw' # id clashes would cause mayhem
+    >>> entry = self.weblog.addEntry(title="Issue #209", id="issue209",
+    ...                             topics=['München'],
+    ...                             excerpt="None", text="None")
+    >>> entry.publish() 
+
+Now we click that topic in the tag cloud. It should lead us to the
+topic view for topic 'München'.
+
+    >>> browser = self.getBrowser()
+    >>> browser.handleErrors = False
+    >>> browser.open('http://nohost/plone/weblog')
+    >>> link = browser.getLink('München')
+    >>> link.click()
+    >>> browser.title
+    '...M\xc3\xbcnchen...'
+
+Now multi topic filtering...
+
+    >>> browser.open('http://nohost/plone/weblog/topics/München/Hamburg/Berlin')
+    >>> browser.title
+    '...M\xc3\xbcnchen...'
+
+Explicit view selection...
+
+    >>> browser.open('http://nohost/plone/weblog/topics/München/@@topic_view')
+    >>> browser.title
+    '...M\xc3\xbcnchen...'
+
+Finally, selecting a non-existant view should raise an exception.
+
+    >>> browser.open('http://nohost/plone/weblog/topics/München/@@notaview')
+    Traceback (most recent call last):
+    ...
+    ComponentLookupError: ...
+    
+While we're at it, let's see if foreign characters in author names
+give us any trouble.
+
+    >>> from Products.CMFCore.utils import getToolByName
+    >>> pmtool = getToolByName(self.portal, 'portal_membership')
+    >>> iAm = pmtool.getAuthenticatedMember()
+    >>> myId  = iAm.getId()
+    >>> oldName = iAm.getProperty('fullname')
+    >>> newName = 'Üsör Ässué180'
+    >>> iAm.setProperties({'fullname': newName})
+    >>> browser.open('http://nohost/plone/weblog/authors/%s' % (myId,))
+    >>> print browser.title
+    Posts by Üsör Ässué180...
+    
+    >>> iAm.setProperties({'fullname': oldName})
Index: /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_quills_app_browser.rst
===================================================================
--- /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_quills_app_browser.rst	(revision 1219)
+++ /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/extras_quills_app_browser.rst	(revision 1219)
@@ -0,0 +1,277 @@
+Quills browser tests
+====================
+
+Here we klick ourselves through a Quills instance and check that everything is in order. First some boilerplate to get our browser up and running:
+
+    >>> self.setRoles(("Contributor",))
+    >>> browser = self.getBrowser(logged_in=True)
+    >>> browser.handleErrors = False
+    >>> entry = self.weblog.addEntry("Blog entry",
+    ...                              "Just for testing",
+    ...                              "Nothing to see.",
+    ...                              ['fishslapping'],
+    ...                              id="entry")
+    >>> from quills.core.interfaces import IWeblogEntry
+    >>> IWeblogEntry.providedBy(entry)
+    True
+
+    >>> entry.publish()
+    >>> browser.open('http://nohost/plone/weblog/')
+
+portlets
+********
+
+Viewing the blog we should get a few portlets. authors, recent entries and the tag cloud:
+
+    >>> browser.contents
+    '...<dl class="portlet portletWeblogAuthors"...'
+
+    >>> browser.contents
+    '...<dl class="portlet portletRecentEntries"...'
+
+    >>> browser.contents
+    '...<dl class="portlet portletWeblogArchive"...'
+
+    >>> browser.contents
+    '...<dl class="portlet portletTagCloud"...
+     ...<a...href="http://nohost/plone/weblog/topics/fishslapping"...
+     ...title="1 entries">fishslapping</a>...'
+
+And since we're authenticated as Contributor, we also get the admin portlet:
+
+    >>> browser.contents
+    '...<dl class="portlet portletWeblogAdmin"...'
+
+And last but not least, the Quills portlet:
+
+    >>> browser.contents
+    '...<dl class="portlet portletQuillsLinks"...'
+
+
+feed links
+**********
+
+When viewing a blog, there should be links to its feeds. We expect atom, rss 2.0 and rdf 1.0 and 1.1:
+
+    >>> browser.contents
+    '...<link...rel="alternate"...
+     ...type="application/atom+xml"...
+     ...title="Atom feed"...
+     ...http://nohost/plone/weblog/atom.xml...'
+
+    >>> browser.contents
+    '...<link...rel="alternate"...
+     ...type="application/rss+xml"...
+     ...title="RSS 2.0 feed"...
+     ...http://nohost/plone/weblog/rss.xml...'
+
+Original quills.app tests - check for presence of following feed links,
+but in quintagroup.quills.extras packages we exclude its from feed as rare
+used feeds. That is way I comment this cheks
+
+#XXX: quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+#    >>> browser.contents
+#    '...<link ...http://nohost/plone/weblog/feed11.rdf...'
+
+#    >>> browser.contents
+#    '...<link ...http://nohost/plone/weblog/feed.rdf...'
+
+the same links should also be present when viewing an entry, but note, we only
+guarantee this when an weblog entry is view within the archive hierarchy, not
+for URLs like 'http://nohost/plone/weblog/entry'.
+
+    >>> date = entry.getPublicationDate()
+    >>> year = str(date.year())
+    >>> month = str(date.month()).zfill(2)
+    >>> day = str(date.day()).zfill(2)
+    >>> archive_path = '%s/%s/%s' % (year, month, day)
+    >>> browser.open('http://nohost/plone/weblog/%s/entry' % archive_path)
+    >>> browser.contents
+    '...<link...rel="alternate"...
+     ...type="application/atom+xml"...
+     ...title="Atom feed"...
+     ...http://nohost/plone/weblog/atom.xml...'
+
+    >>> browser.contents
+    '...<link...rel="alternate"...
+     ...type="application/rss+xml"...
+     ...title="RSS 2.0 feed"...
+     ...http://nohost/plone/weblog/rss.xml...'
+
+Original quills.app tests - check for presence of following feed links,
+but in quintagroup.quills.extras packages we exclude its from feed as rare
+used feeds. That is way I comment this cheks
+
+#XXX: quintagroup.quills.extras fix (because of using quintagorup.plonecomments)
+#    >>> browser.contents
+#    '...<link ...http://nohost/plone/weblog/feed11.rdf...'
+
+#    >>> browser.contents
+#    '...<link ...http://nohost/plone/weblog/feed.rdf...'
+
+
+archive
+*******
+
+having one published entry also gives us an archive:
+
+    >>> date = self.weblog.getEntry('entry').getPublicationDate()
+    >>> year = str(date.year())
+    >>> month = str(date.month()).zfill(2)
+    >>> day = str(date.day()).zfill(2)
+
+    >>> browser.open('http://nohost/plone/weblog/%s/' % year)
+    >>> ('<h1>%s' % year) in browser.contents
+    True
+
+Viewing the archive should still give us a context where the portlets are rendered. We test this by checking for the quillslinks portlet:
+
+    >> browser.contents
+    '...<dl class="portlet portletQuillsLinks"...'
+
+    >>> from Products.CMFPlone.i18nl10n import monthname_english
+    >>> browser.open('http://nohost/plone/weblog/%s/%s/' % (year, month))
+    >>> ('<h1>%s' % monthname_english(month)) in browser.contents
+    True
+
+    >> browser.contents
+    '...<dl class="portlet portletQuillsLinks"...'
+
+    >>> browser.open('http://nohost/plone/weblog/%s/%s/%s/' % (year, month, day))
+    >>> browser.contents
+    '...Blog entry...'
+
+    >>> browser.contents
+    '...<dl class="portlet portletQuillsLinks"...'
+
+topics
+******
+
+    >>> browser.open('http://nohost/plone/weblog/topics')
+    >>> '<div id="weblogtopics">' in browser.contents
+    True
+
+Viewing the topics overview should still give us a context where the portlets are rendered. We test this by checking for the quillslinks portlet:
+
+    >> browser.contents
+    '...<dl class="portlet portletQuillsLinks"...'
+
+
+Having a published entry with the topic 'fishslapping' gives us the following:
+
+    >>> browser.open('http://nohost/plone/weblog/topics/fishslapping')
+    >>> '<div id="topic-summary">' in browser.contents
+    True
+
+    >>> 'Blog entry' in browser.contents
+    True
+
+    >>> '<h1>fishslapping</h1>' in browser.contents
+    True
+
+Viewing the topic view should still give us a context where the portlets are rendered. We test this by checking for the quillslinks portlet:
+
+    >>> browser.contents
+    '...<dl class="portlet portletQuillsLinks"...'
+
+
+author topics
+*************
+
+    >>> browser.open('http://nohost/plone/weblog/authors')
+    >>> '<h1 class="documentFirstHeading">Weblog Authors</h1>' in browser.contents
+    True
+
+    >>> '<a href="http://nohost/plone/weblog/authors/portal_owner">portal_owner</a>' in browser.contents
+    True
+
+
+configure blog
+**************
+
+We need the `Manager` role to configure the weblog:
+
+    >>> self.setRoles(("Manager",))
+
+Now let's configure it ttw:
+
+    >>> browser.open('http://nohost/plone/weblog')
+    >>> browser.getLink('Configure').click()
+    >>> browser.url
+    'http://nohost/plone/weblog/config_view'
+
+But we can also reach this screen via the management portlet:
+
+    >>> browser.getLink('Configure Blog').click()
+    >>> browser.url
+    'http://nohost/plone/weblog/config_view'
+
+
+Breadcrumbs
+***********
+
+Viewing an entry in its archive location should give us breadcrumbs that respect
+the archive location (e.g. 'You are here: Home -> Blog -> 2008 -> December'). To
+test this, first we'll set the publication date to something static that we can
+check for (rather than just 'now'):
+
+    >>> entry = self.weblog.getEntry('entry')
+    >>> from DateTime.DateTime import DateTime
+    >>> entry.setPublicationDate(DateTime('2008/12/16'))
+
+Now let's check the breadcrumb is there for the year:
+
+    >>> browser.open('http://nohost/plone/weblog/2008/12/16/entry')
+    >>> import re
+    >>> s = '<a href="http://nohost/plone/weblog/2008">2008</a>\s*<span class="breadcrumbSeparator">\s*&rarr;'
+    >>> re.search(s, browser.contents) is not None
+    True
+
+We also check that we've got the order of the breadcrumbs correct. During
+development, there was a problem with the entry appearing between 'Home' and
+'Weblog'.
+
+    >>> s = '<a href="http://nohost/plone">Home</a>\s*<span class="breadcrumbSeparator">\s*&rarr;\s*</span>\s*<span dir="ltr">\s*<a href="http://nohost/plone/weblog">Test Weblog</a>'
+    >>> re.search(s, browser.contents) is not None
+    True
+
+
+Manage Comments View
+********************
+
+We'll test the 'manage_comments' view here - the view that is used to help keep
+all comments within a blog in some sense of order.
+
+Can we access the view?
+
+    >>> browser.open('http://nohost/plone/weblog/@@manage_comments')
+
+What about if there are some comments present within the blog? By default we
+need the `Manager' role to add comments:
+
+    >>> self.setRoles(("Contributor", "Reviewer", "Manager"))
+
+We also need to enable comments for the portal type underlying our weblog entry.
+
+    >>> entry = self.weblog.getEntry('entry')
+    >>> # Remember, the return value from getEntry could be catalogbrain-ish, or
+    >>> # an adapter, or an actual content-ish entry object.
+    >>> entry_content = entry.getWeblogEntryContentObject()
+    >>> portal_type = entry_content.portal_type
+
+Now we need to enable commenting for our portal_type.
+
+    >>> dtype = self.portal.portal_types[portal_type]
+    >>> dtype.manage_changeProperties(allow_discussion=1)
+    >>> entry_content.allowDiscussion(1)
+    >>> entry_content.reindexObject()
+
+Let's add a comment to our weblog entry.
+
+    >>> dtool = self.portal.portal_discussion
+    >>> discussion = dtool.getDiscussionFor(entry_content)
+    >>> id = discussion.createReply(title='Parrots', text='... talk like people.')
+
+Now, can we still access the comments view?
+
+    >>> browser.open('http://nohost/plone/weblog/@@manage_comments')
Index: /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/test_docfile.py
===================================================================
--- /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/test_docfile.py	(revision 1218)
+++ /quintagroup.quills.extras/branches/no_webresults/quintagroup/quills/extras/tests/test_docfile.py	(revision 1219)
@@ -1,4 +1,7 @@
 # -*- coding: utf-8 -*-
+import unittest
 from base import *
+from Testing.ZopeTestCase.zopedoctest import ZopeDocFileSuite
+from Products.PloneTestCase.layer import PloneSite
 from Products.QuillsEnabled.tests import test_docfile
 
@@ -8,3 +11,113 @@
 ptc.setupPloneSite(products=PLONE_DEPS)
 
-test_suite = test_docfile.test_suite
+#test_suite = test_docfile.test_suite
+
+def test_suite():
+    suite = unittest.TestSuite(())
+
+    suite.addTest(ZopeDocFileSuite(
+        'workflowstates.txt',
+        package='quills.app.tests',
+        test_class=test_docfile.QuillsDocTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'tests.txt',
+        package='quills.core.tests',
+        test_class=test_docfile.QuillsDocTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'tests.txt',
+        package='quills.core.tests',
+        test_class=test_docfile.QuillsContributorDocTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'tests.txt',
+        package='quills.core.tests',
+        test_class=test_docfile.QuillsDocTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'extras_browser.rst',
+        package='quintagroup.quills.extras.tests',
+        test_class=test_docfile.QuillsFunctionalTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'extras_browser.rst',
+        package='quintagroup.quills.extras.tests',
+        test_class=test_docfile.QuillsFunctionalTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'openBugs.rst',
+        package='Products.QuillsEnabled.tests',
+        test_class=test_docfile.QuillsFunctionalTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'fixedBugs.rst',
+        package='Products.QuillsEnabled.tests',
+        test_class=test_docfile.QuillsFunctionalTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'extras_fixedBugs.rst',
+        package='quintagroup.quills.extras.tests',
+        test_class=test_docfile.QuillsFunctionalTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'openBugs.rst',
+        package='quills.app.tests',
+        test_class=test_docfile.QuillsFunctionalTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'issue193.txt',
+        package='quills.app.tests',
+        test_class=test_docfile.QuillsFunctionalTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'syndication.rst',
+        package='quills.app.tests',
+        test_class=test_docfile.QuillsFunctionalTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.addTest(ZopeDocFileSuite(
+        'tests.txt',
+        package='quills.core.tests',
+        test_class=test_docfile.QuillsContributorDocTestCase,
+        optionflags=test_docfile.optionflags,
+        )
+    )
+
+    suite.layer = PloneSite
+    return suite
