test: Product Query & Filter Engine, Item Group Page

- Test for ProductQuery engine and ProductFilters engine
- Test for engine for Item Group too
- Renamed ‘product_configurator’ to ‘variant_selector’
- Cleaned up filters.py
- Modal freeze backdrop lighter only in cart, since there’s nothing over it
- Fixed unusual spacing in variant selector dialog
- Made `get_child_groups_for_website` more readable
- Replaced ‘Configure’ with ‘Select’ for variant selection
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index f8109ab..68ad702 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -132,7 +132,7 @@
 
 	conditions = " or ".join(conditions)
 
-	from erpnext.e_commerce.product_configurator.utils import get_item_codes_by_attributes
+	from erpnext.e_commerce.variant_selector.utils import get_item_codes_by_attributes
 	possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code]
 
 	for variant in possible_variants:
diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py
index 728d336..01bde29 100644
--- a/erpnext/e_commerce/api.py
+++ b/erpnext/e_commerce/api.py
@@ -8,11 +8,22 @@
 
 from erpnext.e_commerce.product_data_engine.query import ProductQuery
 from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
-from erpnext.setup.doctype.item_group.item_group import get_child_groups
+from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
 
 @frappe.whitelist(allow_guest=True)
 def get_product_filter_data(query_args=None):
-	"""Get filtered products and discount filters."""
+	"""
+		Returns filtered products and discount filters.
+		:param query_args (dict): contains filters to get products list
+
+		Query Args filters:
+		search (str): Search Term.
+		field_filters (dict): Keys include item_group, brand, etc.
+		attribute_filters(dict): Keys include Color, Size, etc.
+		start (int): Offset items by
+		item_group (str): Valid Item Group
+		from_filters (bool): Set as True to jump to page 1
+	"""
 	if isinstance(query_args, str):
 		query_args = json.loads(query_args)
 
@@ -35,7 +46,7 @@
 	sub_categories = []
 	if item_group:
 		field_filters['item_group'] = item_group
-		sub_categories = get_child_groups(item_group)
+		sub_categories = get_child_groups_for_website(item_group, immediate=True)
 
 	engine = ProductQuery()
 	try:
@@ -46,7 +57,7 @@
 			start=start,
 			item_group=item_group
 		)
-	except Exception as e:
+	except Exception:
 		traceback = frappe.get_traceback()
 		frappe.log_error(traceback, frappe._("Product Engine Error"))
 		return {"exc": "Something went wrong!"}
diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py
index 9afca25..4a8e820 100644
--- a/erpnext/e_commerce/doctype/website_item/test_website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -26,6 +26,10 @@
 			"price_list": "_Test Price List India"
 		})
 
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
 	def setUp(self):
 		if self._testMethodName in WEBITEM_DESK_TESTS:
 			make_item("Test Web Item", {
@@ -38,22 +42,13 @@
 				]
 			})
 		elif self._testMethodName in WEBITEM_PRICE_TESTS:
-			self.create_regular_web_item()
+			create_regular_web_item()
 			make_web_item_price(item_code="Test Mobile Phone")
 			make_web_pricing_rule(
 				title="Test Pricing Rule for Test Mobile Phone",
 				item_code="Test Mobile Phone",
 				selling=1)
 
-	def tearDown(self):
-		if self._testMethodName in WEBITEM_DESK_TESTS:
-			frappe.get_doc("Item", "Test Web Item").delete()
-		elif self._testMethodName in WEBITEM_PRICE_TESTS:
-			frappe.delete_doc("Pricing Rule", "Test Pricing Rule for Test Mobile Phone")
-			frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone"}).delete()
-			frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
-
-
 	def test_index_creation(self):
 		"Check if index is getting created in db."
 		from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update
@@ -105,6 +100,8 @@
 		item.reload()
 		self.assertEqual(item.published_in_website, 0)
 
+		item.delete()
+
 	def test_publish_variant_and_template(self):
 		"Check if template is published on publishing variant."
 		# template "Test Web Item" created on setUp
@@ -256,7 +253,7 @@
 			2) Showing stock availability disabled
 		"""
 		item_code = "Test Mobile Phone"
-		self.create_regular_web_item()
+		create_regular_web_item()
 		setup_e_commerce_settings({"show_stock_availability": 1})
 
 		frappe.local.shopping_cart_settings = None
@@ -298,7 +295,7 @@
 		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 
 		item_code = "Test Mobile Phone"
-		self.create_regular_web_item()
+		create_regular_web_item()
 		setup_e_commerce_settings({"show_stock_availability": 1})
 		frappe.local.shopping_cart_settings = None
 
@@ -339,7 +336,7 @@
 	def test_recommended_item(self):
 		"Check if added recommended items are fetched correctly."
 		item_code = "Test Mobile Phone"
-		web_item = self.create_regular_web_item(item_code)
+		web_item = create_regular_web_item(item_code)
 
 		setup_e_commerce_settings({
 			"enable_recommendations": 1,
@@ -347,7 +344,7 @@
 		})
 
 		# create recommended web item and price for it
-		recommended_web_item = self.create_regular_web_item("Test Mobile Phone 1")
+		recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
 		make_web_item_price(item_code="Test Mobile Phone 1")
 
 		# add recommended item to first web item
@@ -379,14 +376,14 @@
 		self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched
 
 		# tear down
-		frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone 1"}).delete()
 		web_item.delete()
 		recommended_web_item.delete()
+		frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
 
 	def test_recommended_item_for_guest_user(self):
 		"Check if added recommended items are fetched correctly for guest user."
 		item_code = "Test Mobile Phone"
-		web_item = self.create_regular_web_item(item_code)
+		web_item = create_regular_web_item(item_code)
 
 		# price visible to guests
 		setup_e_commerce_settings({
@@ -396,7 +393,7 @@
 		})
 
 		# create recommended web item and price for it
-		recommended_web_item = self.create_regular_web_item("Test Mobile Phone 1")
+		recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
 		make_web_item_price(item_code="Test Mobile Phone 1")
 
 		# add recommended item to first web item
@@ -428,22 +425,24 @@
 
 		# tear down
 		frappe.set_user("Administrator")
-		frappe.get_cached_doc("Item Price", {"item_code": "Test Mobile Phone 1"}).delete()
 		web_item.delete()
 		recommended_web_item.delete()
+		frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
 
-	def create_regular_web_item(self, item_code=None):
-		"Create Regular Item and Website Item."
-		item_code = item_code or "Test Mobile Phone"
-		item = make_item(item_code)
+def create_regular_web_item(item_code=None, item_args=None, web_args=None):
+	"Create Regular Item and Website Item."
+	item_code = item_code or "Test Mobile Phone"
+	item = make_item(item_code, properties=item_args)
 
-		if not frappe.db.exists("Website Item", {"item_code": item_code}):
-			web_item = make_website_item(item, save=False)
-			web_item.save()
-		else:
-			web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
+	if not frappe.db.exists("Website Item", {"item_code": item_code}):
+		web_item = make_website_item(item, save=False)
+		if web_args:
+			web_item.update(web_args)
+		web_item.save()
+	else:
+		web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
 
-		return web_item
+	return web_item
 
 def make_web_item_price(**kwargs):
 	item_code = kwargs.get("item_code")
diff --git a/erpnext/e_commerce/product_configurator/test_product_configurator.py b/erpnext/e_commerce/product_configurator/test_product_configurator.py
deleted file mode 100644
index fe4ef08..0000000
--- a/erpnext/e_commerce/product_configurator/test_product_configurator.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import frappe, unittest
-from erpnext.e_commerce.product_data_engine.query import ProductQuery
-from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
-
-test_dependencies = ["Item"]
-#TODO: Rename to test item variant configurator
-
-class TestProductConfigurator(unittest.TestCase):
-	def setUp(self):
-		self.create_variant_item()
-		self.publish_items_on_website()
-
-	# TODO: E-commerce server side tests
-	# def test_product_list(self):
-	# 	template_items = frappe.get_all('Item', {'show_in_website': 1})
-	# 	variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
-
-	# 	products_settings = frappe.get_doc('Products Settings')
-	# 	products_settings.enable_field_filters = 1
-	# 	products_settings.append('filter_fields', {'fieldname': 'item_group'})
-	# 	products_settings.append('filter_fields', {'fieldname': 'stock_uom'})
-	# 	products_settings.save()
-
-	# 	html = get_html_for_route('all-products')
-
-	# 	soup = BeautifulSoup(html, 'html.parser')
-	# 	products_list = soup.find(class_='products-list')
-	# 	items = products_list.find_all(class_='card')
-	# 	self.assertEqual(len(items), len(template_items + variant_items))
-
-	# 	items_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_in_website': 1})
-	# 	variants_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_variant_in_website': 1})
-
-	# 	# mock query params
-	# 	frappe.form_dict = frappe._dict({
-	# 		'field_filters': '{"item_group":["_Test Item Group Desktops"]}'
-	# 	})
-	# 	html = get_html_for_route('all-products')
-	# 	soup = BeautifulSoup(html, 'html.parser')
-	# 	products_list = soup.find(class_='products-list')
-	# 	items = products_list.find_all(class_='card')
-	# 	self.assertEqual(len(items), len(items_with_item_group + variants_with_item_group))
-
-
-	# def test_get_products_for_website(self):
-	# 	items = get_products_for_website(attribute_filters={
-	# 		'Test Size': ['2XL']
-	# 	})
-	# 	self.assertEqual(len(items), 1)
-
-	# def test_products_in_multiple_item_groups(self):
-	# 	"""Check if product is visible on multiple item group pages barring its own."""
-	# 	from erpnext.shopping_cart.product_query import ProductQuery
-
-	# 	if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
-	# 		item_group_doc = frappe.get_doc({
-	# 			"doctype": "Item Group",
-	# 			"item_group_name": "Tech Items",
-	# 			"parent_item_group": "All Item Groups",
-	# 			"show_in_website": 1
-	# 		}).insert()
-	# 	else:
-	# 		item_group_doc = frappe.get_doc("Item Group", "Tech Items")
-
-	# 	doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
-	# 	if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
-	# 		doc.append("website_item_groups", {
-	# 			"item_group": "_Test Item Group Desktops"
-	# 		})
-	# 		doc.save()
-
-	# 	# check if item is visible in its own Item Group's page
-	# 	engine = ProductQuery()
-	# 	result = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
-	# 	items = result["items"]
-
-	# 	self.assertEqual(len(items), 1)
-	# 	self.assertEqual(items[0].item_code, "Portal Item")
-
-	# 	# check if item is visible in configured foreign Item Group's page
-	# 	engine = ProductQuery()
-	# 	result = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
-	# 	items = result["items"]
-	# 	item_codes = [row.item_code for row in items]
-
-	# 	self.assertIn(len(items), [2, 3])
-	# 	self.assertIn("Portal Item", item_codes)
-
-	# 	# teardown
-	# 	doc.delete()
-	# 	item_group_doc.delete()
diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py
index 75137a7..7ae87eb 100644
--- a/erpnext/e_commerce/product_data_engine/filters.py
+++ b/erpnext/e_commerce/product_data_engine/filters.py
@@ -6,7 +6,7 @@
 
 class ProductFiltersBuilder:
 	def __init__(self, item_group=None):
-		if not item_group or item_group == "E Commerce Settings":
+		if not item_group:
 			self.doc = frappe.get_doc("E Commerce Settings")
 		else:
 			self.doc = frappe.get_doc("Item Group", item_group)
@@ -17,36 +17,39 @@
 		if not self.item_group and not self.doc.enable_field_filters:
 			return
 
-		filter_fields = [row.fieldname for row in self.doc.filter_fields]
+		fields, filter_data = [], []
+		filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
 
-		meta = frappe.get_meta('Item')
-		fields = [df for df in meta.fields if df.fieldname in filter_fields]
+		# filter valid field filters i.e. those that exist in Item
+		item_meta = frappe.get_meta('Item', cached=True)
+		fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
 
-		filter_data = []
 		for df in fields:
-			filters, or_filters = {}, []
+			item_filters, item_or_filters = {}, []
+			link_doctype_values = self.get_filtered_link_doctype_records(df)
+
 			if df.fieldtype == "Link":
 				if self.item_group:
-					or_filters.extend([
+					item_or_filters.extend([
 						["item_group", "=", self.item_group],
-						["Website Item Group", "item_group", "=", self.item_group]
+						["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
 					])
 
-				values =  frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
+				# Get link field values attached to published items
+				item_filters['published_in_website'] = 1
+				item_values = frappe.get_all(
+					"Item",
+					fields=[df.fieldname],
+					filters=item_filters,
+					or_filters=item_or_filters,
+					distinct="True",
+					pluck=df.fieldname
+				)
+
+				values = list(set(item_values) & link_doctype_values) # intersection of both
 			else:
-				doctype = df.get_link_doctype()
-
-				# apply enable/disable/show_in_website filter
-				meta = frappe.get_meta(doctype)
-
-				if meta.has_field('enabled'):
-					filters['enabled'] = 1
-				if meta.has_field('disabled'):
-					filters['disabled'] = 0
-				if meta.has_field('show_in_website'):
-					filters['show_in_website'] = 1
-
-				values = [d.name for d in frappe.get_all(doctype, filters)]
+				# table multiselect
+				values = list(link_doctype_values)
 
 			# Remove None
 			if None in values:
@@ -57,6 +60,36 @@
 
 		return filter_data
 
+	def get_filtered_link_doctype_records(self, field):
+		"""
+			Get valid link doctype records depending on filters.
+			Apply enable/disable/show_in_website filter.
+			Returns:
+				set: A set containing valid record names
+		"""
+		link_doctype = field.get_link_doctype()
+		meta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None
+		if meta:
+			filters = self.get_link_doctype_filters(meta)
+			link_doctype_values = set(d.name for d in frappe.get_all(link_doctype, filters))
+
+		return link_doctype_values if meta else set()
+
+	def get_link_doctype_filters(self, meta):
+		"Filters for Link Doctype eg. 'show_in_website'."
+		filters = {}
+		if not meta:
+			return filters
+
+		if meta.has_field('enabled'):
+			filters['enabled'] = 1
+		if meta.has_field('disabled'):
+			filters['disabled'] = 0
+		if meta.has_field('show_in_website'):
+			filters['show_in_website'] = 1
+
+		return filters
+
 	def get_attribute_filters(self):
 		if not self.item_group and not self.doc.enable_attribute_filters:
 			return
@@ -92,9 +125,9 @@
 	def get_discount_filters(self, discounts):
 		discount_filters = []
 
-		# [25.89, 60.5]
+		# [25.89, 60.5] min max
 		min_discount, max_discount = discounts[0], discounts[1]
-		# [25, 60]
+		# [25, 60] rounded min max
 		min_range_absolute, max_range_absolute = floor(min_discount), floor(max_discount)
 		min_range = int(min_discount - (min_range_absolute % 10)) # 20
 		max_range = int(max_discount - (max_range_absolute % 10)) # 60
diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
new file mode 100644
index 0000000..264fbd8
--- /dev/null
+++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
@@ -0,0 +1,116 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import unittest
+
+from erpnext.e_commerce.api import get_product_filter_data
+from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
+
+test_dependencies = ["Item", "Item Group"]
+
+class TestItemGroupProductDataEngine(unittest.TestCase):
+	"Test Products & Sub-Category Querying for Product Listing on Item Group Page."
+
+	@classmethod
+	def setUpClass(cls):
+		item_codes = [
+			("Test Mobile A", "_Test Item Group B"),
+			("Test Mobile B", "_Test Item Group B"),
+			("Test Mobile C", "_Test Item Group B - 1"),
+			("Test Mobile D", "_Test Item Group B - 1"),
+			("Test Mobile E", "_Test Item Group B - 2")
+		]
+		for item in item_codes:
+			item_code = item[0]
+			item_args = {"item_group": item[1]}
+			if not frappe.db.exists("Website Item", {"item_code": item_code}):
+				create_regular_web_item(item_code, item_args=item_args)
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
+	def test_product_listing_in_item_group(self):
+		"Test if only products belonging to the Item Group are fetched."
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B"
+		})
+
+		items = result.get("items")
+		item_codes = [item.get("item_code") for item in items]
+
+		self.assertEqual(len(items), 2)
+		self.assertIn("Test Mobile A", item_codes)
+		self.assertNotIn("Test Mobile C", item_codes)
+
+	def test_products_in_multiple_item_groups(self):
+		"""Test if product is visible on multiple item group pages barring its own."""
+		website_item = frappe.get_doc("Website Item", {"item_code": "Test Mobile E"})
+
+		# show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well
+		website_item.append("website_item_groups", {
+			"item_group": "_Test Item Group B - 1"
+		})
+		website_item.save()
+
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B - 1"
+		})
+
+		items = result.get("items")
+		item_codes = [item.get("item_code") for item in items]
+
+		self.assertEqual(len(items), 3)
+		self.assertIn("Test Mobile E", item_codes) # visible in other item groups
+		self.assertIn("Test Mobile C", item_codes)
+		self.assertIn("Test Mobile D", item_codes)
+
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B - 2"
+		})
+
+		items = result.get("items")
+
+		self.assertEqual(len(items), 1)
+		self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group
+
+	def test_item_group_with_sub_groups(self):
+		"Test Valid Sub Item Groups in Item Group Page."
+		frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
+		frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
+
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B"
+		})
+
+		self.assertTrue(bool(result.get("sub_categories")))
+
+		child_groups = [d.name for d in result.get("sub_categories")]
+		# check if child group is fetched if shown in website
+		self.assertIn("_Test Item Group B - 1", child_groups)
+
+		frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B"
+		})
+		child_groups = [d.name for d in result.get("sub_categories")]
+
+		# check if child group is fetched if shown in website
+		self.assertIn("_Test Item Group B - 1", child_groups)
+		self.assertIn("_Test Item Group B - 2", child_groups)
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
index 8bca046..9a7cb3c 100644
--- a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
+++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
@@ -2,37 +2,337 @@
 # For license information, please see license.txt
 
 import frappe
+import unittest
 
-test_dependencies = ["Item"]
+from erpnext.e_commerce.product_data_engine.query import ProductQuery
+from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
+from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
+from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import setup_e_commerce_settings
+
+test_dependencies = ["Item", "Item Group"]
 
 class TestProductDataEngine(unittest.TestCase):
-	"Test Products Querying for Product Listing."
-	def test_product_list_ordering(self):
-		"Check if website items appear by ranking."
-		pass
+	"Test Products Querying and Filters for Product Listing."
 
-	def test_product_list_paging(self):
-		pass
+	@classmethod
+	def setUpClass(cls):
+		item_codes = [
+			("Test 11I Laptop", "Products"), # rank 1
+			("Test 12I Laptop", "Products"), # rank 2
+			("Test 13I Laptop", "Products"), # rank 3
+			("Test 14I Laptop", "Raw Material"), # rank 4
+			("Test 15I Laptop", "Raw Material"), # rank 5
+			("Test 16I Laptop", "Raw Material"), # rank 6
+			("Test 17I Laptop", "Products") # rank 7
+		]
+		for index, item in enumerate(item_codes, start=1):
+			item_code = item[0]
+			item_args = {"item_group": item[1]}
+			web_args = {"ranking": index}
+			if not frappe.db.exists("Website Item", {"item_code": item_code}):
+				create_regular_web_item(item_code, item_args=item_args, web_args=web_args)
+
+		setup_e_commerce_settings({
+			"products_per_page": 4,
+			"enable_field_filters": 1,
+			"filter_fields": [{"fieldname": "item_group"}],
+			"enable_attribute_filters": 1,
+			"filter_attributes": [{"attribute": "Test Size"}]
+		})
+		frappe.local.shopping_cart_settings = None
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
+	def test_product_list_ordering_and_paging(self):
+		"Test if website items appear by ranking on different pages."
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields={},
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		self.assertIsNotNone(items)
+		self.assertEqual(len(items), 4)
+		self.assertGreater(result.get("items_count"), 4)
+
+		# check if items appear as per ranking set in setUpClass
+		self.assertEqual(items[0].get("item_code"), "Test 17I Laptop")
+		self.assertEqual(items[1].get("item_code"), "Test 16I Laptop")
+		self.assertEqual(items[2].get("item_code"), "Test 15I Laptop")
+		self.assertEqual(items[3].get("item_code"), "Test 14I Laptop")
+
+		# check next page
+		result = engine.query(
+			attributes={},
+			fields={},
+			search_term=None,
+			start=4,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if items appear as per ranking set in setUpClass on next page
+		self.assertEqual(items[0].get("item_code"), "Test 13I Laptop")
+		self.assertEqual(items[1].get("item_code"), "Test 12I Laptop")
+		self.assertEqual(items[2].get("item_code"), "Test 11I Laptop")
+
+	def test_change_product_ranking(self):
+		"Test if item on second page appear on first if ranking is changed."
+		item_code = "Test 12I Laptop"
+		old_ranking = frappe.db.get_value("Website Item", {"item_code": item_code}, "ranking")
+
+		# low rank, appears on second page
+		self.assertEqual(old_ranking, 2)
+
+		# set ranking as highest rank
+		frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", 10)
+
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields={},
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if item is the first item on the first page
+		self.assertEqual(items[0].get("item_code"), item_code)
+		self.assertEqual(items[1].get("item_code"), "Test 17I Laptop")
+
+		# tear down
+		frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", old_ranking)
+
+	def test_product_list_field_filter_builder(self):
+		"Test if field filters are fetched correctly."
+		frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 0)
+
+		filter_engine = ProductFiltersBuilder()
+		field_filters = filter_engine.get_field_filters()
+
+		# Web Items belonging to 'Products' and 'Raw Material' are available
+		# but only 'Products' has 'show_in_website' enabled
+		item_group_filters = field_filters[0]
+		docfield = item_group_filters[0]
+		valid_item_groups = item_group_filters[1]
+
+		self.assertEqual(docfield.options, "Item Group")
+		self.assertIn("Products", valid_item_groups)
+		self.assertNotIn("Raw Material", valid_item_groups)
+
+		frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 1)
+		field_filters = filter_engine.get_field_filters()
+
+		#'Products' and 'Raw Materials' both have 'show_in_website' enabled
+		item_group_filters = field_filters[0]
+		docfield = item_group_filters[0]
+		valid_item_groups = item_group_filters[1]
+
+		self.assertEqual(docfield.options, "Item Group")
+		self.assertIn("Products", valid_item_groups)
+		self.assertIn("Raw Material", valid_item_groups)
 
 	def test_product_list_with_field_filter(self):
-		pass
+		"Test if field filters are applied correctly."
+		field_filters = {"item_group": "Raw Material"}
+
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields=field_filters,
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if only 'Raw Material' are fetched in the right order
+		self.assertEqual(len(items), 3)
+		self.assertEqual(items[0].get("item_code"), "Test 16I Laptop")
+		self.assertEqual(items[1].get("item_code"), "Test 15I Laptop")
+
+	# def test_product_list_with_field_filter_table_multiselect(self):
+	# 	TODO
+	# 	pass
+
+	def test_product_list_attribute_filter_builder(self):
+		"Test if attribute filters are fetched correctly."
+		create_variant_web_item()
+
+		filter_engine = ProductFiltersBuilder()
+		attribute_filter = filter_engine.get_attribute_filters()[0]
+		attribute = attribute_filter.item_attribute_values[0]
+
+		self.assertEqual(attribute_filter.name, "Test Size")
+		self.assertEqual(len(attribute_filter.item_attribute_values), 1)
+		self.assertEqual(attribute.attribute_value, "Large")
 
 	def test_product_list_with_attribute_filter(self):
-		pass
+		"Test if attribute filters are applied correctly."
+		create_variant_web_item()
 
-	def test_product_list_with_discount_filter(self):
-		pass
+		attribute_filters = {"Test Size": ["Large"]}
+		engine = ProductQuery()
+		result = engine.query(
+			attributes=attribute_filters,
+			fields={},
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
 
-	def test_product_list_with_mixed_filtes(self):
-		pass
+		# check if only items with Test Size 'Large' are fetched
+		self.assertEqual(len(items), 1)
+		self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
 
-	def test_product_list_with_mixed_filtes_item_group(self):
-		pass
+	def test_product_list_discount_filter_builder(self):
+		"Test if discount filters are fetched correctly."
+		from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price, make_web_pricing_rule
 
-	def test_products_in_multiple_item_groups(self):
-		"Check if product is visible on multiple item group pages barring its own."
-		pass
+		item_code = "Test 12I Laptop"
+		make_web_item_price(item_code=item_code)
+		make_web_pricing_rule(
+			title=f"Test Pricing Rule for {item_code}",
+			item_code=item_code,
+			selling=1
+		)
+
+		setup_e_commerce_settings({"show_price": 1})
+		frappe.local.shopping_cart_settings = None
+
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields={},
+			search_term=None,
+			start=4,
+			item_group=None
+		)
+
+		self.assertTrue(bool(result.get("discounts")))
+
+		filter_engine = ProductFiltersBuilder()
+		discount_filters = filter_engine.get_discount_filters(result["discounts"])
+
+		self.assertEqual(len(discount_filters[0]), 2)
+		self.assertEqual(discount_filters[0][0], 10)
+		self.assertEqual(discount_filters[0][1], "10% and above")
+
+	def test_product_list_with_discount_filters(self):
+		"Test if discount filters are applied correctly."
+		from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price, make_web_pricing_rule
+
+		field_filters = {"discount": [10]}
+
+		make_web_item_price(item_code="Test 12I Laptop")
+		make_web_pricing_rule(
+			title="Test Pricing Rule for Test 12I Laptop", # 10% discount
+			item_code="Test 12I Laptop",
+			selling=1
+		)
+		make_web_item_price(item_code="Test 13I Laptop")
+		make_web_pricing_rule(
+			title="Test Pricing Rule for Test 13I Laptop", # 15% discount
+			item_code="Test 13I Laptop",
+			discount_percentage=15,
+			selling=1
+		)
+
+		setup_e_commerce_settings({"show_price": 1})
+		frappe.local.shopping_cart_settings = None
+
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields=field_filters,
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if only product with 10% and above discount are fetched in the right order
+		self.assertEqual(len(items), 2)
+		self.assertEqual(items[0].get("item_code"), "Test 13I Laptop")
+		self.assertEqual(items[1].get("item_code"), "Test 12I Laptop")
+
+	def test_product_list_with_api(self):
+		"Test products listing using API."
+		from erpnext.e_commerce.api import get_product_filter_data
+
+		create_variant_web_item()
+
+		result = get_product_filter_data(query_args={
+			"field_filters": {
+				"item_group": "Products"
+			},
+			"attribute_filters": {
+				"Test Size": ["Large"]
+			},
+			"start": 0
+		})
+
+		items = result.get("items")
+
+		self.assertEqual(len(items), 1)
+		self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
 
 	def test_product_list_with_variants(self):
-		pass
+		"Test if variants are hideen on hiding variants in settings."
+		create_variant_web_item()
 
+		setup_e_commerce_settings({
+			"enable_attribute_filters": 0,
+			"hide_variants": 1
+		})
+		frappe.local.shopping_cart_settings = None
+
+		attribute_filters = {"Test Size": ["Large"]}
+		engine = ProductQuery()
+		result = engine.query(
+			attributes=attribute_filters,
+			fields={},
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if any variants are fetched even though published variant exists
+		self.assertEqual(len(items), 0)
+
+		# tear down
+		setup_e_commerce_settings({
+			"enable_attribute_filters": 1,
+			"hide_variants": 0
+		})
+
+def create_variant_web_item():
+	"Create Variant and Template Website Items."
+	from erpnext.stock.doctype.item.test_item import make_item
+	from erpnext.controllers.item_variant import create_variant
+	from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+
+	make_item("Test Web Item", {
+		"has_variant": 1,
+		"variant_based_on": "Item Attribute",
+		"attributes": [
+			{
+				"attribute": "Test Size"
+			}
+		]
+	})
+	if not frappe.db.exists("Item", "Test Web Item-L"):
+		variant = create_variant("Test Web Item", {"Test Size": "Large"})
+		variant.save()
+
+	if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}):
+		make_website_item(variant, save=True)
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_configurator/__init__.py b/erpnext/e_commerce/variant_selector/__init__.py
similarity index 100%
rename from erpnext/e_commerce/product_configurator/__init__.py
rename to erpnext/e_commerce/variant_selector/__init__.py
diff --git a/erpnext/e_commerce/product_configurator/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
similarity index 100%
rename from erpnext/e_commerce/product_configurator/item_variants_cache.py
rename to erpnext/e_commerce/variant_selector/item_variants_cache.py
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
new file mode 100644
index 0000000..3eeca17
--- /dev/null
+++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py
@@ -0,0 +1,10 @@
+# import frappe
+import unittest
+# from erpnext.e_commerce.product_data_engine.query import ProductQuery
+# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+
+test_dependencies = ["Item"]
+
+class TestVariantSelector(unittest.TestCase):
+	# TODO: Variant Selector Tests
+	pass
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_configurator/utils.py b/erpnext/e_commerce/variant_selector/utils.py
similarity index 98%
rename from erpnext/e_commerce/product_configurator/utils.py
rename to erpnext/e_commerce/variant_selector/utils.py
index 5ea32f9..2e1852c 100644
--- a/erpnext/e_commerce/product_configurator/utils.py
+++ b/erpnext/e_commerce/variant_selector/utils.py
@@ -1,7 +1,6 @@
 import frappe
 from frappe.utils import cint
-
-from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager
+from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
 
 def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
 	items = []
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index c719d50..bfca3f4 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -502,12 +502,7 @@
 }
 
 .item-configurator-dialog {
-	.modal-header {
-		padding: var(--padding-md) var(--padding-xl);
-	}
-
 	.modal-body {
-		padding: 0 var(--padding-xl);
 		padding-bottom: var(--padding-xl);
 
 		.status-area {
@@ -1292,13 +1287,10 @@
 	font-size: 72px;
 }
 
-.modal-backdrop {
-	position: fixed;
-	top: 0;
-	right: 0;
-	left: 0;
-	background-color: var(--gray-100);
-	height: 100%;
+[data-path="cart"] {
+	.modal-backdrop {
+		background-color: var(--gray-50); // lighter backdrop only on cart freeze
+	}
 }
 
 .item-thumb {
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index f4b667e..3d1a1d0 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -104,16 +104,23 @@
 	def delete_child_item_groups_key(self):
 		frappe.cache().hdel("child_item_groups", self.name)
 
-	def validate_item_group_defaults(self):
-		from erpnext.stock.doctype.item.item import validate_item_default_company_links
-		validate_item_default_company_links(self.item_group_defaults)
-
-def get_child_groups(item_group_name):
+def get_child_groups_for_website(item_group_name, immediate=False):
 	"""Returns child item groups *excluding* passed group."""
-	item_group = frappe.get_doc("Item Group", item_group_name)
-	return frappe.db.sql("""select name, route
-		from `tabItem Group` where lft>%(lft)s and rgt<%(rgt)s
-			and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt}, as_dict=1)
+	item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
+	filters = {
+		"lft": [">", item_group.lft],
+		"rgt": ["<", item_group.rgt],
+		"show_in_website": 1
+	}
+
+	if immediate:
+		filters["parent_item_group"] = item_group_name
+
+	return frappe.get_all(
+		"Item Group",
+		filters=filters,
+		fields=["name", "route"]
+	)
 
 def get_child_item_groups(item_group_name):
 	item_group = frappe.get_cached_value("Item Group",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index de023ca..52e3b40 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -914,7 +914,7 @@
 
 def invalidate_item_variants_cache_for_website(doc):
 	"""Rebuild ItemVariantsCacheManager via Item or Website Item."""
-	from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager
+	from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
 
 	item_code = None
 	is_web_item = doc.get("published_in_website") or doc.get("published")
diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html
index 9ff1d79..fcab594 100644
--- a/erpnext/templates/generators/item/item_configure.html
+++ b/erpnext/templates/generators/item/item_configure.html
@@ -3,11 +3,11 @@
 
 <div class="mt-5 mb-6">
 	{% if cart_settings.enable_variants | int %}
-	<button class="btn btn-primary-light btn-configure font-md"
+	<button class="btn btn-primary-light btn-configure font-md mr-2"
 		data-item-code="{{ doc.name }}"
 		data-item-name="{{ doc.item_name }}"
 	>
-		{{ _('Configure') }}
+		{{ _('Select Variant') }}
 	</button>
 	{% endif %}
 	{% if cart_settings.show_contact_us_button %}
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
index f47650a..3220226 100644
--- a/erpnext/templates/generators/item/item_configure.js
+++ b/erpnext/templates/generators/item/item_configure.js
@@ -29,7 +29,7 @@
 		});
 
 		this.dialog = new frappe.ui.Dialog({
-			title: __('Configure {0}', [this.item_name]),
+			title: __('Select Variant for {0}', [this.item_name]),
 			fields,
 			on_hide: () => {
 				set_continue_configuration();
@@ -280,14 +280,14 @@
 	}
 
 	get_next_attribute_and_values(selected_attributes) {
-		return this.call('erpnext.e_commerce.product_configurator.utils.get_next_attribute_and_values', {
+		return this.call('erpnext.e_commerce.variant_selector.utils.get_next_attribute_and_values', {
 			item_code: this.item_code,
 			selected_attributes
 		});
 	}
 
 	get_attributes_and_values() {
-		return this.call('erpnext.e_commerce.product_configurator.utils.get_attributes_and_values', {
+		return this.call('erpnext.e_commerce.variant_selector.utils.get_attributes_and_values', {
 			item_code: this.item_code
 		});
 	}
@@ -311,9 +311,9 @@
 	const { itemCode } = $btn_configure.data();
 
 	if (localStorage.getItem(`configure:${itemCode}`)) {
-		$btn_configure.text(__('Continue Configuration'));
+		$btn_configure.text(__('Continue Selection'));
 	} else {
-		$btn_configure.text(__('Configure'));
+		$btn_configure.text(__('Select Variant'));
 	}
 }