def set_individual_price(self, row: WebElement, price: str, num: int = 0, enter=True): """ Sets price on a listing using inline edit :param row: Listing row web element :param price: string value to set as a price :param num: index of price input, starting from 0 (first price input) :param enter: confirm the price by pressing Enter after sending keys """ try: # click on div to reveal input(s) div_elements = row.find_elements(*self._product_price_div) self.scroll_to_element(div_elements[num], True) click(div_elements[num]) # wait at least for first input to show up self.wait_for_child_element(row, self._product_price_input) # find input and set the price inputs = self.product_price_inputs(row) inputs[num].clear() if enter: price = price + Keys.ENTER send_keys(inputs[num], price) except IndexError: print('Invalid price input index: ' + str(num)) raise
def test_filter_search(self): """ Tests the filter search """ d = self.driver pg = MainPage(d) expected_filters = [{ 'section': 'CATEGORIES', 'items': ['Clothing\n2'] }, { 'section': 'SECTION', 'items': ['On Sale\n1', 'Summer Sale\n1'] }, { 'section': 'TAGS', 'items': ['Tag01\n1', 'Tag02\n1'] }, { 'section': 'MATERIALS', 'items': ['iron\n1', 'wool\n1'] }] pg.select_filter_tab('Active') sleep(1) srch = pg.filter_search() send_keys(srch, 'fi') send_keys(srch, Keys.RETURN) sleep(1) actual_filters = self.get_filters() assert actual_filters == expected_filters listings = pg.listing_titles_sorted() assert listings == ['Fifth something', 'First something 1234']
def test_create_tag_special_chars(self): """ Tests that a tag can be created in bulk edit with czech chars, but not special chars """ expected_tags = [ ['Tag01', 'žvýkačky'], ['Tag01', 'žvýkačky'], ['Tag01', 'žvýkačky'], ] select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) send_keys(bp.operation_input(), 'žvýkačky') click(bp.operation_apply()) apply_class = bp.operation_apply().get_attribute('class') assert 'inactive' in apply_class.split(' ') tag_names = bp.tag_names() assert tag_names == expected_tags send_keys(bp.operation_input(), 'me@site') err = bp.error_baloon() assert err == "Tag can only include spaces, letters, hyphens, and numbers"
def find_n_replace(self, what, for_what): find_input = self.driver.find_element(By.CSS_SELECTOR, 'div.bulk-edit--find input') send_keys(find_input, what) replace_input = self.driver.find_element( By.CSS_SELECTOR, 'div.bulk-edit--replace input') send_keys(replace_input, for_what)
def search(self, text: str): """ Search records using search input :param text: text to search """ field = self.driver.find_element(*self.SEARCH_FIELD) send_keys(field, text)
def test_title_replace(self): """ Tests title replace - basic test """ expected_listings_1 = [ 'Prvni something 1234 (1)\n116 characters remaining', 'Second something 1235 (2)\n115 characters remaining', 'Third something LG-512a (3)\n113 characters remaining' ] select_listings_to_edit(self.driver, 'Find & Replace') d = self.driver bp = BulkPage(d) input_find_field = bp.operation_input_find() input_replace_field = bp.operation_input_replace() # Normal Replace send_keys(input_find_field, 'First') send_keys(input_replace_field, 'Prvni') listings = bp.listing_rows_texts_sorted() assert listings == expected_listings_1 # Apply (client only) click(bp.operation_apply()) sleep(1) listings = bp.listing_rows_texts_sorted() assert listings == expected_listings_1 apply_class = bp.operation_apply().get_attribute('class') assert 'inactive' in apply_class.split(' ')
def test_edit_single_description(self): """ Tests that single description can be edited, including special characters """ select_listings_to_edit(self.driver) d = self.driver mp = MainPage(d) bp = BulkPage(d) row = bp.listing_row('First something 1234 (1)') description = row.find_element_by_css_selector( 'div.body span.description') assert description.text == 'invisible gloves' click(description) sleep(1) form = row.find_element_by_css_selector('div.body form > textarea') click(form) send_keys(form, ' Hello<b> & > 1') click( d.find_element_by_css_selector('bulk-edit-dashboard-op-container')) sleep(1) description_text = row.find_element_by_css_selector( 'div.body span.description').text assert description_text == 'invisible gloves Hello<b> & > 1'
def test_title_add_before_length(self): """ Tests add before where length limit is exceeded """ long_text = 'Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong' expected_listings_1 = [ long_text + 'First something 1234 (1)\n2 characters remaining', long_text + 'Second something 1235 (2)\n1 character remaining', long_text + 'Third something LG-512a (3)\n1 character over limit' ] expected_listings_2 = [ long_text + 'First something 1234 (1)\n2 characters remaining', long_text + 'Second something 1235 (2)\n1 character remaining', 'Third something LG-512a (3)\n113 characters remaining' ] select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) input_field = bp.operation_input() # Test long text send_keys(input_field, long_text) listings = bp.listing_rows_texts_sorted() assert listings == expected_listings_1 # Apply (client only) click(bp.operation_apply()) sleep(1) listings = bp.listing_rows_texts_sorted() assert listings == expected_listings_2 apply_class = bp.operation_apply().get_attribute('class') assert 'inactive' in apply_class.split(' ')
def test_title_delete(self): """ Tests tile delete basic """ expected_listings_1 = [ 'First something 1234 (1)\n122 characters remaining', 'Second something 1235 (2)\n115 characters remaining', 'Third something LG-512a (3)\n113 characters remaining' ] expected_listings_2 = [ 'Second something 1235 (2)\n115 characters remaining', 'Third something LG-512a (3)\n113 characters remaining', 'something 1234 (1)\n122 characters remaining' ] select_listings_to_edit(self.driver, 'Delete') d = self.driver bp = BulkPage(d) input_field = bp.operation_input() # Normal Delete send_keys(input_field, 'First ') listings = bp.listing_rows_texts_sorted() assert listings == expected_listings_1 # Apply (client only) click(bp.operation_apply()) sleep(1) listings = bp.listing_rows_texts_sorted() assert listings == expected_listings_2 apply_class = bp.operation_apply().get_attribute('class') assert 'inactive' in apply_class.split(' ')
def set_individual_sku(self, row: WebElement, sku: str, num: int = 0, enter=True): """ Sets sku on a listing using inline edit :param row: Listing row web element :param sku: string value to set as a sku :param num: index of sku input, starting from 0 (first sku input) :param enter: confirm the sku by pressing Enter after sending keys """ try: # click on div to reveal input(s) div_elements = row.find_elements(*self._product_sku_div) click(div_elements[num]) # wait at least for first input to show up self.wait_for_child_element(row, self._product_sku_input) # find input and set the price inputs = self.product_sku_inputs(row) inputs[num].clear() if enter: sku = sku + Keys.ENTER send_keys(inputs[num], sku) except IndexError: print('Invalid sku input index: ' + str(num)) raise
def test_delete_tag(self): """ Tests that tags can be deleted in bulk """ expected_tags = [ [], [], [], ] select_listings_to_edit(self.driver, 'Delete') d = self.driver bp = BulkPage(d) tag_field = bp.operation_input() click(tag_field) send_keys(tag_field, 'Tag') send_keys(tag_field, '01') click(bp.operation_apply()) apply_class = bp.operation_apply().get_attribute('class') assert 'inactive' in apply_class.split(' ') tag_names = bp.tag_names() assert tag_names == expected_tags
def test_sync_updates_tags(self): """ Tests that data is written to the database when [Sync Updates] is clicked """ expected_data = [['1', '{Tag01,AAA,BBB,CCC}'], ['2', '{Tag01,AAA,BBB,CCC}'], ['3', '{Tag01,AAA,BBB,CCC}']] select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) send_keys(bp.operation_input(), 'AAA,BBB ,CCC') click(bp.operation_apply()) # Check apply button assert bp.operation_apply().is_enabled( ) is False, 'Apply button is enabled' click(bp.sync_updates_button()) # Check updated data in DB wait_for_assert(expected_data, lambda: run_sql('HIVE', 'select_tags_modified', True), 'Unexpected tags data in DB')
def test_inventory_sku_change_to(self): """ Tests Change to operation for SKU editor :return: """ expected_listings = [ 'Product #1 without variations\n550', 'Product #2 with one variation with pricing\nCHANGE', 'Product #3 with two variations with quantity on both and pricing on both\nCHANGE' ] # Change SKUs on 2nd and 3rd listing operation = 'Change To' bis = BulkPageInventorySku(self.driver, self.ts) bis.select_operation(operation) # unselect 1st bis.click_on_listings(['Product #1 without variations']) # enter new SKU send_keys(bis.sku_input(), 'CHANGE') # Apply and check results click(bis.operation_apply()) wait_for_web_assert(False, bis.operation_apply().is_enabled, 'Apply button is not disabled') assert bis.listing_rows_texts_sorted() == expected_listings
def test_sync_updates_title(self): """ Tests that data is written to the database when [Sync Updates] is clicked It also tests that data are correctly processed in more than one batch (see HIVE-1216). """ # Configure Hive to process changes in batches of two listings # Env variable must be set before Hive is started os.environ['SYNC_UPDATES_BATCH_SIZE'] = '2' expected_data = [['1', 'hello First something 1234 (1)'], ['2', 'hello Second something 1235 (2)'], ['3', 'hello Third something LG-512a (3)']] select_listings_to_edit(self.driver) bp = BulkPage(self.driver) input_field = bp.operation_input() send_keys(input_field, 'hello ') # click on Apply and check Apply button click(bp.operation_apply()) wait_for_web_assert(False, bp.operation_apply().is_enabled, 'Apply button is enabled') # Sync changes click(bp.sync_updates_button()) # Check data in DB wait_for_assert(expected_data, lambda: run_sql('HIVE', 'select_title_modified', True), 'Unexpected title data in DB')
def test_sync_description(self): """ Tests that data is written to the database when [Sync Updates] is clicked """ expected_data = [['1', 'invisible gloves New Description']] d = self.driver select_listings_to_edit(d) bp = BulkPage(d) row = bp.listing_row('First something 1234 (1)') description = row.find_element_by_css_selector( 'div.body span.description') click(description) sleep(1) form = row.find_element_by_css_selector('div.body form > textarea') click(form) send_keys(form, ' New Description') click( d.find_element_by_css_selector('bulk-edit-dashboard-op-container')) sleep(1) click(bp.sync_updates_button()) wait_for_assert( expected_data, lambda: run_sql('HIVE', 'select_description_modified', True), 'Unexpected data in DB')
def set_individual_quantity(self, row: WebElement, quantity: str, num: int = 0, enter=True): """ Sets price on a listing using inline edit :param row: Listing row web element :param quantity: string value to set as a quantity :param num: index of quantity input, starting from 0 (first quantity input) :param enter: confirm the quantity by pressing Enter after sending keys """ try: # click on div to reveal input(s) div_elements = row.find_elements(*self._product_quantity_div) click(div_elements[num]) # wait at least for first input to show up self.wait_for_child_element(row, self._product_quantity_input) # find input and set the quantity inputs = self.product_quantity_inputs(row) inputs[num].clear() if enter: quantity = quantity + Keys.ENTER send_keys(inputs[num], quantity) except IndexError: print('Invalid quantity input index: ' + str(num)) raise
def test_description_discard(self): """ Tests that the single description changes are not discarded when a user starts editing a new one """ select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) row = bp.listing_row('First something 1234 (1)') description = row.find_element_by_css_selector( 'div.body span.description') assert description.text == 'invisible gloves' click(description) sleep(1) form = row.find_element_by_css_selector('div.body form > textarea') click(form) send_keys(form, ' Test') # click on the second description row2 = bp.listing_row('Second something 1235 (2)') description2 = row2.find_element_by_css_selector( 'div.body span.description') click(description2) # check the 1st description is saved row = bp.listing_row('First something 1234 (1)') description = row.find_element_by_css_selector( 'div.body span.description') assert description.text == 'invisible gloves Test'
def test_single_description_required(self): """ Tests that single description cannot be changed to empty """ select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) row = bp.listing_row('First something 1234 (1)') description = row.find_element_by_css_selector( 'div.body span.description') click(description) sleep(1) # delete description and check error message form = row.find_element_by_css_selector('div.body form > textarea') click(form) send_keys(form, BACKSPACE_KEYS * 4) sleep(1) error_text = bp.error_baloon_texts(row) assert error_text == ['Description is required'] # click away and check that description was not changed click( d.find_element_by_css_selector('bulk-edit-dashboard-op-container')) description_text = row.find_element_by_css_selector( 'div.body span.description').text assert description_text == 'invisible gloves'
def test_inventory_quantity_invalid_bulk(self): """ Tests that a quantity invalid characters cannot be entered """ biq = BulkPageInventoryQuantity(self.driver, self.ts) input_field = biq.operation_input() send_keys(input_field, 'foo') err = biq.error_baloon() assert err == "Use a whole number between 0 and 999" assert biq.operation_apply().is_enabled() is False, 'Apply button is not disabled' send_keys(input_field, BACKSPACE_KEYS + ' 123') err = biq.error_baloon() assert err == "Use a whole number between 0 and 999" send_keys(input_field, BACKSPACE_KEYS + '0x10') err = biq.error_baloon() assert err == "At least one offering must be in stock" send_keys(input_field, BACKSPACE_KEYS + '-1') err = biq.error_baloon() assert err == "Use a whole number between 0 and 999" send_keys(input_field, BACKSPACE_KEYS + '1.45') err = biq.error_baloon() assert err == "Use a whole number between 0 and 999"
def set_single_title(self, row_title, new_title): row = self.listing_row(row_title, self.TITLE_ROW_SELECTOR) title_element = row.find_element(*self.TITLE_ELEMENT_REL_LOCATOR) click(title_element) input_element = self.wait_for_child_element( row, self.TITLE_INPUT_REL_LOCATOR) input_element.clear() send_keys(input_element, new_title + Keys.ENTER) self.wait_for_child_element(row, self.TITLE_ELEMENT_REL_LOCATOR)
def add_new_section(self, section_name): section_dropdown = self.section_dropdown() self.open_dropdown(section_dropdown, individual_dropdown=False) sleep(1) try: self.operation_menu_new_item_input().clear() except NoSuchElementException: raise MaxSectionsReachedError('Input element was not found') send_keys(self.operation_menu_new_item_input(), section_name + Keys.RETURN)
def test_create_tag_too_long(self): """ Tests that a tag cannot be longer than 20 characters """ select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) send_keys(bp.operation_input(), 'AAAAABBBBBCCCCCDDDDDE') err = bp.error_baloon() assert err == "Maximum length of tag is 20"
def test_create_tag_multi_over(self): """ Tests that multiple tags can be created in bulk edit, but no more than 13 """ expected_tags_01 = [ ['Tag01'], ['Tag01'], ['Tag01', 'AAA', 'BBB', 'CCC'], ] expected_tags_02 = [ [ 'Tag01', '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11' ], [ 'Tag01', '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11' ], [ 'Tag01', 'AAA', 'BBB', 'CCC', '00', '01', '02', '03', '04', '05', '06', '07', '08' ], ] select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) # deselect 2, 3 bp.click_on_listings( ['Second something 1235 (2)', 'Third something LG-512a (3)']) # append AAA BBB CCC tags to the 1st listing send_keys(bp.operation_input(), 'AAA,BBB ,CCC') click(bp.operation_apply()) apply_class = bp.operation_apply().get_attribute('class') assert 'inactive' in apply_class.split(' ') tag_names = bp.tag_names() assert tag_names == expected_tags_01 # select 2, 3 again bp.click_on_listings( ['Second something 1235 (2)', 'Third something LG-512a (3)']) send_keys( bp.operation_input(), '00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15') click(bp.operation_apply()) apply_class = bp.operation_apply().get_attribute('class') assert 'inactive' in apply_class.split(' ') tag_names = bp.tag_names() assert tag_names == expected_tags_02
def test_inventory_update_single_price(self): """ Tests that single price can be updated """ expected_listings_02 = { 'Product #1 without variations': '$10.50', 'Product #2 with one variation with pricing': [('Beige', '$1.00'), ('Black', '$2.00'), ('Blue', '$3.00'), ('Silver', '$4.00'), ('White', '$5.00'), ('Yellow', '$6.00'), ('Custom color 1', '$7.00'), ('Custom color 2', '$8.00')], 'Product #3 with two variations with quantity on both and pricing on both': [ ('XXS US Women\'s', 'Material 1', '$99.00'), ('XXS US Women\'s', 'Material 2', '$99.00'), ('XXS US Women\'s', 'Material 3', '$99.00'), ('One size (plus) US Women\'s', 'Material 1', '$99.00'), ('One size (plus) US Women\'s', 'Material 2', '$99.00'), ('One size (plus) US Women\'s', 'Material 3', '$99.00'), ('Custom size 1 US Women\'s', 'Material 1', '$99.00'), ('Custom size 1 US Women\'s', 'Material 2', '$99.00'), ('Custom size 1 US Women\'s', 'Material 3', '$99.00'), ] } bip = BulkPageInventoryPrice(self.driver, self.ts) actual_listings = bip.listing_details() self.check_listing_options(actual_listings, expected_listings_01) # unselect 1st bip.click_on_listings(['Product #1 without variations']) # unselect 2nd bip.click_on_listings(['Product #2 with one variation with pricing']) # perform individual (inline) change of listing #1 row = bip.listing_row('Product #1 without variations') assert bip.product_price_text(row) == '$500.00' bip.set_individual_price(row, '10.50') # perform bulk change of selected listings (only 3rd) bip.select_operation('Change To') input_field = bip.operation_input_dolars() send_keys(input_field, '99') # apply changes click(bip.operation_apply()) wait_for_web_assert(False, bip.operation_apply().is_enabled, 'Apply button is enabled') # check listings actual_listings = bip.listing_details() self.check_listing_options(actual_listings, expected_listings_02)
def test_inventory_increase_price_dollars(self): """ Tests that a price can be increased in bulk edit """ expected_listings_02 = { 'Product #1 without variations': '$510.00', 'Product #2 with one variation with pricing': [ ('Beige', '$11.00'), ('Black', '$12.00'), ('Blue', '$13.00'), ('Silver', '$14.00'), ('White', '$15.00'), ('Yellow', '$16.00'), ('Custom color 1', '$17.00'), ('Custom color 2', '$18.00'), ], 'Product #3 with two variations with quantity on both and pricing on both': [ ('XXS US Women\'s', 'Material 1', '$10.00'), ('XXS US Women\'s', 'Material 2', '$20.00'), ('XXS US Women\'s', 'Material 3', '$30.00'), ('One size (plus) US Women\'s', 'Material 1', '$40.00'), ('One size (plus) US Women\'s', 'Material 2', '$50.00'), ('One size (plus) US Women\'s', 'Material 3', '$60.00'), ('Custom size 1 US Women\'s', 'Material 1', '$70.00'), ('Custom size 1 US Women\'s', 'Material 2', '$80.00'), ('Custom size 1 US Women\'s', 'Material 3', '$90.00'), ] } operation = 'Increase By' bip = BulkPageInventoryPrice(self.driver, self.ts) actual_listings = bip.listing_details() self.check_listing_options(actual_listings, expected_listings_01) # unselect 3rd bip.click_on_listings([ 'Product #3 with two variations with quantity on both and pricing on both' ]) bip.select_operation(operation) input_field = bip.operation_input_dolars() send_keys(input_field, '10') actual_listings = bip.listing_details() self.check_listing_options(actual_listings, expected_listings_02) # Apply changes click(bip.operation_apply()) actual_listings = bip.listing_details() self.check_listing_options(actual_listings, expected_listings_02) assert bip.operation_apply().is_enabled( ) is False, 'Apply button is enabled'
def change_individual_sku(self, product_name, sku): row = self.product_row_by_name(product_name) click(row.find_element(By.CSS_SELECTOR, 'div.checkbox'), delay=2) sku_text = row.find_element_by_css_selector( 'div.body div.property-value-column') click(sku_text) input_individual = self.product_sku_input(row) input_individual.clear() send_keys(input_individual, sku) sleep(0.3) click( self.driver.find_element( By.CSS_SELECTOR, '.bulk-edit--selected > div:nth-child(1)'))
def test_inventory_price_invalid_bulk(self): """ Tests that a price invalid characters cannot be entered """ bip = BulkPageInventoryPrice(self.driver, self.ts) input_field = bip.operation_input() send_keys(input_field, 'foo') err = bip.error_baloon() assert err == "Must be a number" assert bip.operation_apply().is_enabled( ) is False, 'Apply button is not disabled' send_keys(input_field, BACKSPACE_KEYS + ' 123') err = bip.error_baloon() assert err == "Must be a number" send_keys(input_field, BACKSPACE_KEYS + '0x10') err = bip.error_baloon() assert err == "Must be a number" send_keys(input_field, BACKSPACE_KEYS + '-1') err = bip.error_baloon() assert err == "Must be positive number"
def test_title_add_before_starting_chars(self): """ Tests that add before title starts with valid chars """ expected_listings_1 = [ '123First something 1234 (1)\n113 characters remaining', '123Second something 1235 (2)\n112 characters remaining', '123Third something LG-512a (3)\n110 characters remaining' ] expected_listings_2 = [ '@ First something 1234 (1)\nMust begin with alphanumerical character', '@ Second something 1235 (2)\nMust begin with alphanumerical character', '@ Third something LG-512a (3)\nMust begin with alphanumerical character' ] expected_listings_3 = [ 'á First something 1234 (1)\n114 characters remaining', 'á Second something 1235 (2)\n113 characters remaining', 'á Third something LG-512a (3)\n111 characters remaining' ] select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) input_field = bp.operation_input() # Test 123 prefix - OK send_keys(input_field, '123') listings = bp.listing_rows_texts_sorted() error_msg = bp.error_baloon() assert error_msg == '' assert listings == expected_listings_1 # Test @ prefix - show error, temp title does not contain it send_keys(input_field, BACKSPACE_KEYS) send_keys(input_field, '@ ') sleep(2) listings = bp.listing_rows_texts_sorted() error_msg = bp.error_baloon() assert error_msg == 'Must begin with alphanumerical character' assert listings == expected_listings_2 # Test á prefix - no error, temp title contains it send_keys(input_field, BACKSPACE_KEYS) send_keys(input_field, 'á ') listings = bp.listing_rows_texts_sorted() error_msg = bp.error_baloon() assert error_msg == '' assert listings == expected_listings_3
def test_create_material_multi_over(self): """ Tests that multiple materials can be created in bulk edit, but no more than 13 """ expected_materials_01 = [ ['cotton'], ['cotton'], ['wool', 'AAA', 'BBB', 'CCC'], ] expected_materials_02 = [ [ 'cotton', '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11' ], [ 'cotton', '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11' ], [ 'wool', 'AAA', 'BBB', 'CCC', '00', '01', '02', '03', '04', '05', '06', '07', '08' ], ] select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) # deselect 2, 3 bp.click_on_listings( ['Second something 1235 (2)', 'Third something LG-512a (3)']) # append AAA BBB CCC materials to the 1st listing send_keys(bp.operation_input(), 'AAA,BBB ,CCC') click(bp.operation_apply()) material_names = bp.material_names() assert material_names == expected_materials_01 # append 00, 01, 02... to all listings bp.click_on_listings( ['Second something 1235 (2)', 'Third something LG-512a (3)']) send_keys( bp.operation_input(), '00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15') click(bp.operation_apply()) material_names = bp.material_names() assert material_names == expected_materials_02
def test_create_material_too_long(self): """ Tests that a material cannot be longer than 45 characters """ select_listings_to_edit(self.driver) d = self.driver bp = BulkPage(d) send_keys(bp.operation_input(), 'AAAAaBBBBbCCCCcDDDDdEEEEeFFFFfGGGGgHHHHhIIIIi') err = bp.error_baloon() assert err == "" send_keys(bp.operation_input(), 'J') err = bp.error_baloon() assert err == "Materials must be 45 characters or less."