def view_rights_propagate_to_a(fixture): """The access rights specified for a View are propagated to an A, made from a Bookmark to that View.""" fixture.view.write_check = EmptyStub() fixture.view.read_check = EmptyStub() a = A.from_bookmark(fixture.view, fixture.view.as_bookmark()) vassert( a.read_check is fixture.view.read_check ) vassert( a.write_check is fixture.view.write_check )
def resources(self, fixture): """A Resource has the responsibility to handle an HTTP Request. A Resource indicates that it can handle a particular HTTP method by having a method named for it. Such method should return a Response.""" @stubclass(Resource) class ResourceStub(Resource): called = None @exempt def handle_something(self, request): self.called = True return 'something' @exempt def handle_anotherthing(self, request): pass resource = ResourceStub() # Case where the HTTP method is not supported fixture.request.method = 'koos' response = resource.handle_request(fixture.request) vassert(isinstance(response, HTTPMethodNotAllowed)) vassert(not resource.called) vassert(response.headers['allow'] == 'anotherthing, something') # Case where the HTTP method is supported fixture.request.method = 'SOMEthing' response = resource.handle_request(fixture.request) vassert(resource.called) vassert(response == 'something')
def persistence(fixture): fixture.start_example_app() fixture.driver_browser.open('/') vassert(not fixture.driver_browser.is_element_present('//h1')) vassert(fixture.driver_browser.is_element_present('//form')) fixture.driver_browser.type(XPath.input_labelled('Email address'), '*****@*****.**') fixture.driver_browser.type(XPath.input_labelled('Comment'), 'some comment text') fixture.driver_browser.capture_cropped_screenshot( fixture.new_screenshot_path('persistence1.png')) fixture.driver_browser.click(XPath.button_labelled('Submit')) vassert(fixture.driver_browser.is_element_present('//form')) vassert( fixture.driver_browser.is_element_present( '//form/following-sibling::div/p')) comment_text = fixture.driver_browser.get_text( '//form/following-sibling::div/p') vassert(comment_text == 'By [email protected]: some comment text') fixture.driver_browser.capture_cropped_screenshot( fixture.new_screenshot_path('persistence2.png'))
def ui_slots_map_to_window(self, fixture): """The UserInterface uses its own names for Slots. When attaching a UserInterface, you have to specify which of the UserInterface's Slots plug into which of the page's Slots. """ class UIWithSlots(UserInterface): def assemble(self): root = self.define_view('/', title='UserInterface root view') root.set_slot( 'text', P.factory(text='in user_interface slot named text')) class MainUI(UserInterface): def assemble(self): self.define_page(HTML5Page).use_layout( PageLayout( contents_layout=ColumnLayout('main').with_slots())) self.define_user_interface('/a_ui', UIWithSlots, {'text': 'main'}, name='myui') wsgi_app = fixture.new_wsgi_app(site_root=MainUI) browser = Browser(wsgi_app) browser.open('/a_ui/') vassert(browser.title == 'UserInterface root view') # The widget in the UserInterface's slot named 'text' end up in the HTML5Page slot called main [p ] = browser.lxml_html.xpath('//div[contains(@class,"column-main")]/p') vassert(p.text == 'in user_interface slot named text')
def basic_workings(self, fixture): """A DhtmlUI provides a UserInterface which maps to the filesystem where there may be a combination of .d.html and other files. When a d.html file is requested from it, the contents of the specified div from inside the d.html file is inserted into the specified Slot. When a normal file is requested, the file is sent verbatim.""" class MainUI(UserInterface): def assemble(self): self.define_page(HTML5Page).use_layout( PageLayout( contents_layout=ColumnLayout('main').with_slots())) self.define_user_interface('/dhtml_ui', DhtmlUI, {'main_slot': 'main'}, name='test_ui', static_div_name='astatic') # Djhtml files should be located in the web.static_root fixture.config.web.static_root = fixture.static_dir.name wsgi_app = fixture.new_wsgi_app(site_root=MainUI, enable_js=True) browser = Browser(wsgi_app) # A dhtml file: HTML5Page's main_slot now contains the insides of the div in the dhtml file browser.open('/dhtml_ui/correctfile.d.html') html = fixture.get_inserted_html(browser) vassert(html == fixture.div_internals) # A non-dhtml file is returned verbatim browser.open('/dhtml_ui/otherfile.txt') contents = browser.raw_html vassert(contents == 'other') # Files that do not exist are reported as such browser.open('/dhtml_ui/idonotexist.txt', status=404) browser.open('/dhtml_ui/idonotexist.d.html', status=404)
def version_dictates_execution_of_migration_(fixture): """Each Migration should have a class attribute `version` that states which version of the component it upgrades the database schema to. Only the Migrations with versions greater than the current schema version are included in a MigrationRun for a given egg. """ class PreviousVersionMigration(Migration): version = '1.0' class MatchingCurrentVersionMigration(Migration): version = '2.0' class NewerVersionMigration(Migration): version = '3.0' class EvenNewerVersionMigration(Migration): version = '4.0' egg = ReahlEggStub('my_egg', '4.0', [ PreviousVersionMigration, MatchingCurrentVersionMigration, NewerVersionMigration, EvenNewerVersionMigration ]) fixture.orm_control.set_currently_installed_version_for(egg, '2.0') migration_run = MigrationRun(fixture.orm_control, [egg]) migrations_to_run = migration_run.migrations_to_run_for(egg) classes_to_run = [m.__class__ for m in migrations_to_run] vassert( classes_to_run == [NewerVersionMigration, EvenNewerVersionMigration])
def ui_arguments(self, fixture): """UserInterfaces can take exta args and kwargs.""" class UIWithArguments(UserInterface): def assemble(self, kwarg=None): self.kwarg = kwarg text = self.kwarg root = self.define_view('/', title='A view') root.set_slot('text', P.factory(text=text)) class MainUI(UserInterface): def assemble(self): self.define_page(HTML5Page).use_layout( PageLayout( contents_layout=ColumnLayout('main').with_slots())) self.define_user_interface('/a_ui', UIWithArguments, {'text': 'main'}, name='myui', kwarg='the kwarg') wsgi_app = fixture.new_wsgi_app(site_root=MainUI) browser = Browser(wsgi_app) browser.open('/a_ui/') [p] = browser.lxml_html.xpath('//p') vassert(p.text == 'the kwarg')
def adding_checkboxes(fixture): """CheckboxInputs are added non-inlined, and by default without labels.""" class DomainObjectWithBoolean(object): @exposed def fields(self, fields): fields.an_attribute = BooleanField(label='Some input', required=True) fixture.domain_object = DomainObjectWithBoolean() class FormWithInputWithCheckbox(Form): def __init__(self, view): super(FormWithInputWithCheckbox, self).__init__(view, 'aform') self.use_layout(FormLayout()) self.layout.add_input( CheckboxInput(self, fixture.domain_object.fields.an_attribute)) browser = Browser( fixture.new_wsgi_app( child_factory=FormWithInputWithCheckbox.factory())) browser.open('/') vassert(not any(child.tag == 'label' for child in fixture.get_form_group_children(browser))) [div] = fixture.get_form_group_children(browser) [checkbox] = div.getchildren() vassert(checkbox.attrib['class'] == 'checkbox')
def interface_with_meta_info(self, fixture): """A Reahl component can publish a ReahlEgg instance to supply extra meta information about itself. Such interfaces with extra information are also often used from a flattened list in dependency order.""" easter_egg.clear() easter_egg.add_dependency('reahl-component') # The interface for a component is published via the reahl.eggs entry point line = 'Egg = reahl.component.eggs:ReahlEgg' easter_egg.add_entry_point_from_line('reahl.eggs', line) # Interfaces can be queried in dependency order too interfaces_in_order = ReahlEgg.compute_all_relevant_interfaces( easter_egg.as_requirement_string()) vassert(len(interfaces_in_order) == 2) # That of reahl-component itself, and of the easteregg [interface ] = [i for i in interfaces_in_order if i.distribution is easter_egg] # The meta-info that can be obtained via such an interface vassert(interface.configuration_spec is None) orm_control = EmptyStub() vassert(interface.get_persisted_classes_in_order(orm_control) == []) vassert(interface.migrations_in_order == []) vassert(interface.get_roles_to_add() == []) # Hooks for allowing a component to do its own housekeeping with expected(NoException): interface.do_daily_maintenance()
def checked_arguments(self, fixture): """A CheckedRemoteMethod checks and marshalls its parameters using Fields.""" def callable_object(anint=None, astring=None): fixture.method_kwargs = {'anint': anint, 'astring': astring} return '' remote_method = CheckedRemoteMethod('amethod', callable_object, MethodResult(), immutable=fixture.immutable, anint=IntegerField(), astring=Field()) wsgi_app = fixture.new_wsgi_app(remote_method=remote_method) browser = Browser(wsgi_app) if fixture.immutable: browser.open( '/_amethod_method?anint=5&astring=SupercalifraGilisticexpialidocious' ) else: browser.post('/_amethod_method', { 'anint': '5', 'astring': 'SupercalifraGilisticexpialidocious' }) vassert(fixture.method_kwargs == { 'anint': 5, 'astring': 'SupercalifraGilisticexpialidocious' })
def query_string_prepopulates_form(self, fixture): """Widget query string arguments can be used on forms to pre-populate inputs based on the query string.""" class ModelObject(object): @exposed def fields(self, fields): fields.arg_on_other_object = Field() class FormWithQueryArguments(Form): def __init__(self, view): self.model_object = ModelObject() super(FormWithQueryArguments, self).__init__(view, 'name') self.add_child( TextInput(self, self.model_object.fields.arg_on_other_object)) @exposed def query_fields(self, fields): fields.arg_on_other_object = self.model_object.fields.arg_on_other_object wsgi_app = fixture.new_wsgi_app( widget_factory=FormWithQueryArguments.factory()) browser = Browser(wsgi_app) browser.open('/?arg_on_other_object=metoo') vassert(browser.lxml_html.xpath('//input')[0].value == 'metoo')
def entry_point_class_list(self, fixture): """EntryPointClassList is a special ConfigSetting which reads its value from a pkg_resources entry point which contains a list of classes published by any (possibly other) egg.""" # Because we cannot remove EasterEggs from pkg_resources, the next test must happen after # this one. The next check just ensures that we know when that does not happen: with expected(pkg_resources.DistributionNotFound): pkg_resources.require('test-inject') fixture.set_config_spec( easter_egg, 'reahl.component_dev.test_config:ConfigWithEntryPointClassList') # Publish some classes on the entry point being tested line = 'ListedClass1 = reahl.component_dev.test_config:ListedClass1' easter_egg.add_entry_point_from_line('some.test.entrypoint', line) line = 'ListedClass2 = reahl.component_dev.test_config:ListedClass2' easter_egg.add_entry_point_from_line('some.test.entrypoint', line) # Usually this happens inside other infrastructure, such as the implementation of reahl serve (see reahl-dev) config = StoredConfiguration(fixture.config_dir.name) config.configure() # The classes are found from the entry point vassert( set(config.some_key.some_setting) == {ListedClass1, ListedClass2})
def is_version_controlled(self, fixture): non_initialised_directory = fixture.new_bzr_directory(initialised=False) bzr = Bzr(non_initialised_directory.name) vassert( not bzr.is_version_controlled() ) bzr = Bzr(fixture.bzr_directory.name) vassert( bzr.is_version_controlled() )
def dynamically_determining_attributes(fixture): """A Widget can determine its attribute values at the latest possible stage, based on changing data.""" class WidgetWithDynamicAttributes(HTMLElement): state = '1' @property def attributes(self): attributes = super(WidgetWithDynamicAttributes, self).attributes attributes.set_to('dynamic', self.state) attributes.add_to('dynamiclist', [self.state]) attributes.add_to('not-there', ['a value']) attributes.remove_from('not-there', ['a value']) return attributes widget = WidgetWithDynamicAttributes(fixture.view, 'x') widget.set_attribute('fixed', 'value1') tester = WidgetTester(widget) widget.state = '1' rendered = tester.render_html() vassert( rendered == '<x dynamic="1" dynamiclist="1" fixed="value1">' ) widget.state = '2' rendered = tester.render_html() vassert( rendered == '<x dynamic="2" dynamiclist="2" fixed="value1">' )
def egg_schema_version_init(fixture): orm_control = SqlAlchemyControl() egg = ReahlEggStub('initegg', '0.0', []) orm_control.create_db_tables(None, [egg]) current_version = orm_control.schema_version_for(egg) vassert(current_version == egg.version)
def rendering_inputs(self, fixture): """How Inputs render, depending on the rights.""" tester = WidgetTester(fixture.input_widget) actual = tester.render_html() vassert(actual == fixture.expected_html)
def schedule_executes_in_order(fixture): """A MigrationSchedule is used internally to schedule calls in different phases. The calls scheduled in each phase are executed in the order the phases have been set up on the MigrationSchedule. Within a phase, the calls are executed in the order they were registered in that phase. """ schedule_names = ['a', 'b', 'c'] migration_schedule = MigrationSchedule(*schedule_names) class SomeObject(object): def do_something(self, arg): pass some_object = SomeObject() #schedule calls not in registered order with CallMonitor(some_object.do_something) as monitor: migration_schedule.schedule('c', some_object.do_something, 'c1') migration_schedule.schedule('a', some_object.do_something, 'a1') migration_schedule.schedule('b', some_object.do_something, 'b') migration_schedule.schedule('a', some_object.do_something, 'a2') migration_schedule.schedule('c', some_object.do_something, 'c2') migration_schedule.execute_all() actual_order = [call.args[0] for call in monitor.calls] expected_order = ['a1', 'a2', 'b', 'c1', 'c2'] vassert(actual_order == expected_order)
def non_writable_events_are_dealt_with_like_invalid_input(self, fixture): """If a form submits an Event with access rights that prohibit writing, a ValidationException is raised.""" class ModelObject(object): @exposed def events(self, events): events.an_event = Event( label='click me', writable=Allowed(False), disallowed_message='you cannot do this') model_object = ModelObject() class TestPanel(Div): def __init__(self, view): super(TestPanel, self).__init__(view) form = self.add_child(Form(view, 'some_form')) form.define_event_handler(model_object.events.an_event) button = form.add_child( ButtonInput(form, model_object.events.an_event)) if button.validation_error: form.add_child(form.create_error_label(button)) fixture.form = form wsgi_app = fixture.new_wsgi_app(child_factory=TestPanel.factory()) browser = Browser(wsgi_app) browser.open('/') browser.post(fixture.form.event_channel.get_url().path, {'event.an_event?': ''}) browser.follow_response() input_id = browser.get_id_of('//input[@name="event.an_event?"]') error_label = browser.get_html_for('//label') vassert(error_label == '<label for="%s" class="error">you cannot do this</label>' % input_id)
def how_migration_works(fixture): """Calls that will modify the database are scheduled in the schedule_upgrades() method of all the applicable Migrations for a single migration run. `shedule_upgrades()` is called on each migration in order of their versions. Once all calls are scheduled, they are executed as scheduled. """ class SomeObject(object): calls_made = [] def do_something(self, arg): self.calls_made.append(arg) some_object = SomeObject() class Migration1(Migration): version = '2.0' def schedule_upgrades(self): self.schedule('drop_fk', some_object.do_something, 'drop_fk_1') self.schedule('data', some_object.do_something, 'data_1') self.schedule('drop_fk', some_object.do_something, 'drop_fk_2') class Migration2(Migration): version = '3.0' def schedule_upgrades(self): self.schedule('drop_fk', some_object.do_something, 'drop_fk_3') egg = ReahlEggStub('my_egg', '4.0', [Migration1, Migration2]) fixture.orm_control.set_currently_installed_version_for(egg, '1.0') fixture.orm_control.migrate_db([egg]) expected_order = ['drop_fk_1', 'drop_fk_2', 'drop_fk_3', 'data_1'] vassert(some_object.calls_made == expected_order)
def table_thead(fixture): """Table can find its Thead element""" table = Table(fixture.view) thead = table.add_child(Thead(fixture.view)) vassert(table.thead is thead)
def basic_ui(self, fixture): """A UserInterface is a chunk of web app that can be grafted onto the URL hierarchy of any app. A UserInterface has its own views. Its Views are relative to the UserInterface itself. """ class UIWithTwoViews(UserInterface): def assemble(self): self.define_view('/', title='UserInterface root view') self.define_view('/other', title='UserInterface other view') class MainUI(UserInterface): def assemble(self): self.define_page(HTML5Page) self.define_user_interface('/a_ui', UIWithTwoViews, {}, name='myui') wsgi_app = fixture.new_wsgi_app(site_root=MainUI) browser = Browser(wsgi_app) browser.open('/a_ui/') vassert(browser.title == 'UserInterface root view') browser.open('/a_ui/other') vassert(browser.title == 'UserInterface other view')
def input_wrapped_widgets(fixture): """An Input is an empty Widget; its contents are supplied by overriding its .create_html_widget() method. Several methods for setting HTML-things, like css id are delegated to this Widget which represents the Input in HTML. """ class MyInput(PrimitiveInput): def create_html_widget(self): return HTMLElement(self.view, 'x') test_input = MyInput(fixture.form, fixture.field) tester = WidgetTester(test_input) rendered = tester.render_html() vassert(rendered == '<x>') test_input.set_id('myid') test_input.set_title('mytitle') test_input.add_to_attribute('list-attribute', ['one', 'two']) test_input.set_attribute('an-attribute', 'a value') rendered = tester.render_html() vassert( rendered == '<x id="myid" an-attribute="a value" list-attribute="one two" title="mytitle">' )
def i18n_dhtml(self, fixture): """Djhtml files can have i18nsed versions, which would be served up if applicable.""" @stubclass(WebExecutionContext) class AfrikaansContext(WebExecutionContext): @property def interface_locale(self): return 'af' class MainUI(UserInterface): def assemble(self): self.define_page(HTML5Page).use_layout( PageLayout( contents_layout=ColumnLayout('main').with_slots())) self.define_user_interface('/dhtml_ui', DhtmlUI, {'main_slot': 'main'}, name='test_ui', static_div_name='astatic') # Djhtml files should be located in the web.static_root fixture.config.web.static_root = fixture.static_dir.name wsgi_app = fixture.new_wsgi_app(site_root=MainUI) browser = Browser(wsgi_app) # request the file, but get the transalated alternative for the locale def stubbed_create_context_for_request(): return AfrikaansContext() with replaced(wsgi_app.create_context_for_request, stubbed_create_context_for_request): browser.open('/dhtml_ui/correctfile.d.html') vassert(browser.title == 'Afrikaans bo!')
def visibility(self, fixture): """Widgets are rendered only if their .visible property is True.""" class MyMessage(Widget): is_visible = True def __init__(self, view): super(MyMessage, self).__init__(view) self.add_child(P(view, text='你好世界!')) @property def visible(self): return self.is_visible message = MyMessage(fixture.view) widget_tester = WidgetTester(message) # Case: when visible message.is_visible = True actual = widget_tester.render_html() vassert(actual == '<p>你好世界!</p>') # Case: when not visible message.is_visible = False actual = widget_tester.render_html() vassert(actual == '')
def prevent_duplicate_upload_js(fixture): """The user is prevented from uploading more than one file with the same name on the client side. """ error_locator = XPath.span_containing( 'uploaded files should all have different names') def error_is_visible(): return browser.is_visible(error_locator) fixture.reahl_server.set_app(fixture.new_wsgi_app(enable_js=True)) browser = fixture.driver_browser browser.open('/') browser.type(XPath.input_labelled('Choose file(s)'), fixture.file_to_upload1.name) browser.wait_for_not(error_is_visible) browser.type(XPath.input_labelled('Choose file(s)'), fixture.file_to_upload2.name) browser.wait_for_not(error_is_visible) with fixture.reahl_server.paused(): browser.type(XPath.input_labelled('Choose file(s)'), fixture.file_to_upload1.name) vassert( not fixture.upload_file_is_queued(fixture.file_to_upload1.name)) browser.wait_for(error_is_visible) browser.click( XPath.button_labelled('Remove', filename=fixture.file_to_upload2_name)) browser.wait_for_not(error_is_visible)
def rendering_of_constraints(self, fixture): """The constraints of the Field of an PrimitiveInput are rendered in html as html attributes of an Input which corresponds with the name of each validation_constraint and has value the parameters of the validation_constraint. The error message of each validation_constraint is also put in a json object inside the class attribute. These measures make it possible to write constraints that are checked on the browser either by browser support or by a jquery validate add-on, and in case of failure to display the exact corresponding error message.""" fixture.field.bind('an_attribute', fixture.model_object) fixture.model_object.an_attribute = 'field value' constraint1 = ValidationConstraint('validation_constraint 1 message') constraint1.name = 'one' @stubclass(ValidationConstraint) class ConstraintWithParams(ValidationConstraint): @property def parameters(self): return 'a parameter' constraint2 = ConstraintWithParams( 'validation_constraint 2 message with apostrophe\'s') constraint2.name = 'two' fixture.field.add_validation_constraint(constraint1) fixture.field.add_validation_constraint(constraint2) tester = WidgetTester(fixture.input) actual = tester.render_html() escaped_json = html_escape( '{"validate": {"messages": {"data-one": "validation_constraint 1 message", "data-two": "validation_constraint 2 message with apostrophe\\\\\'s"}}}' ) expected_html = '''<input name="an_attribute" data-one="true" data-two="a parameter" form="test" type="inputtype" value="field value" class="%s">''' % escaped_json vassert(actual == expected_html)
def read_text_object(self, fixture): fixture.reader.register(fixture.tag_name, fixture.test_class) read_object = fixture.reader.read_file(fixture.file, None) if fixture.text: vassert(read_object.text == fixture.text) else: vassert(not hasattr(read_object, 'text'))
def placeholder_with_text(fixture): placeholder = PlaceholderImage(fixture.view, 20, 30, text='My banner') expected_value = 'holder.js/20x30?text=My banner' actual_value = placeholder.get_attribute('data-src') vassert(actual_value == expected_value)
def tabs_with_sub_options(fixture): """A TabbedPanel can have Tabs that are each composed of multiple sub-options.""" fixture.request.query_string = 'tab=mult2' tabbed_panel = TabbedPanel(fixture.view) multi_tab = MultiTab(fixture.view, 'tab 1 name', 'multitab-main') multi_tab.add_tab( Tab(fixture.view, 'multi tab 1', 'mult1', P.factory(text='tab 1/1 content'))) multi_tab.add_tab( Tab(fixture.view, 'multi tab 2', 'mult2', P.factory(text='tab 1/2 content'))) tabbed_panel.add_tab(multi_tab) tester = WidgetTester(tabbed_panel) expected_html = \ '''<ul class="nav nav-tabs reahl-menu">'''\ '''<li class="dropdown nav-item">'''\ '''<a data-target="-" data-toggle="dropdown" href="/?open_item=tab+1+name&tab=mult2" class="active dropdown-toggle nav-link reahl-ajaxlink">tab 1 name<span class="caret"></span></a>'''\ '''<div class="dropdown-menu">'''\ '''<a data-target="#tab_mult1" data-toggle="tab" href="/?tab=mult1" class="dropdown-item">multi tab 1</a>'''\ '''<a data-target="#tab_mult2" data-toggle="tab" href="/?tab=mult2" class="active dropdown-item">multi tab 2</a>'''\ '''</div>'''\ '''</li>'''\ '''</ul>'''\ '''<div class="tab-content">'''\ '''<div id="tab_mult1" class="tab-pane"><p>tab 1/1 content</p></div>'''\ '''<div id="tab_mult2" class="active tab-pane"><p>tab 1/2 content</p></div>'''\ '''</div>''' actual = tester.render_html() vassert(actual == expected_html)
def distinguishing_identical_field_names(fixture): """A programmer can add different Inputs on the same Form even if their respective Fields are bound to identically named attributes of different objects.""" class ModelObject(object): @exposed def fields(self, fields): fields.field_name = IntegerField() model_object1 = ModelObject() model_object2 = ModelObject() class MyForm(Form): @exposed def events(self, events): events.an_event = Event(label='click me') def __init__(self, view, name): super(MyForm, self).__init__(view, name) self.define_event_handler(self.events.an_event) self.add_child(ButtonInput(self, self.events.an_event)) self.add_child(TextInput(self, model_object1.fields.field_name)) self.add_child(TextInput(self, model_object2.fields.field_name)) wsgi_app = fixture.new_wsgi_app(child_factory=MyForm.factory('form')) fixture.reahl_server.set_app(wsgi_app) fixture.driver_browser.open('/') # the correct input value gets to the correct object despite referencing identically named attributes fixture.driver_browser.type('//input[@type="text"][1]', '0') fixture.driver_browser.type('//input[@type="text"][2]', '1') fixture.driver_browser.click(XPath.button_labelled('click me')) vassert(model_object1.field_name == 0) vassert(model_object2.field_name == 1)