diff --git a/README.md b/README.md index 276ac60b9725aa839e60f25a0d0e4550d72b6b4c..942139a6940b2b21bcb5fec628565254347b4a0d 100644 --- a/README.md +++ b/README.md @@ -58,4 +58,16 @@ Pada tutorial **chapter 7**, saya membuat 2 file html yang berbeda, yaitu `index Pada tutorial **chapter 8**, saya belajar tentang Django Template Inheritance dengan menambahkan `base.html` yang nantinya akan di-inherits oleh file html lainnya. Selain itu saya juga belajar untuk menggunakan static files seperti bootstrap. Saya juga mencoba membuat file css tersendiri untuk sedikit memodifikasi tampilan. -Keterhubungan perubahan code pada chapter 8 ini dengan chapter sebelumnya (chapter 7) adalah adanya penggunaan `base.html` untuk memudahkan tampilan pada `index.html` dan `list.html` yang memiliki kemiripan. Kedua html file tersebut sama-sama memiliki input text, namun ada beberapa perbedaan seperti header text dan juga action yang ada pada form. Dengan menggunakan `base.html` seperti yang dijelaskan pada chapter 8, kita tidak perlu menuliskan code untuk tampilan yang sama sebanyak dua kali (**Don't repeat yourself!**), namun cukup menuliskannya pada `base.html` sedangkan tampilan yang berbeda akan dihandle oleh masing-masing html yang berkaitan. \ No newline at end of file +Keterhubungan perubahan code pada chapter 8 ini dengan chapter sebelumnya (chapter 7) adalah adanya penggunaan `base.html` untuk memudahkan tampilan pada `index.html` dan `list.html` yang memiliki kemiripan. Kedua html file tersebut sama-sama memiliki input text, namun ada beberapa perbedaan seperti header text dan juga action yang ada pada form. Dengan menggunakan `base.html` seperti yang dijelaskan pada chapter 8, kita tidak perlu menuliskan code untuk tampilan yang sama sebanyak dua kali (**Don't repeat yourself!**), namun cukup menuliskannya pada `base.html` sedangkan tampilan yang berbeda akan dihandle oleh masing-masing html yang berkaitan. + + +## Exercise 5 + +Pada penerapan refactoring (red, green, refactor), `red` berarti developer membuat failing test dari implementasi yang nantinya akan dibuat. Test ini dipastikan failed karena production code belum dituliskan. Test yang dibuat haruslah bersifat readability, artinya test tersebut mudah dipahami agar implementasinya pada production code bisa menjadi lebih mudah. + +Selanjutnya `green` berarti developer membuat production code yang akan membuat test yang telah dibuat sebelumnya menjadi passed. + +Sedangkan `refactor` berarti developer menambahkan atau memodifikasi production code yang sudah dibuat sebelumnya agar code tersebut lebih optimal serta dapat meningkatkan readability dan maintanability. Pada refactor ini biasanya developer akan membersihkan code smells yang ada pada production code, seperti code yang duplicate, method yang terlalu panjang, dan code smells lainnya. Refactor seharusnya tidak mengubah implementasi dari production code yang telah dibuat, namun hanya mengubah bagaimana implementasi tersebut dijalankan dengan lebih rapi. + + +Test Organizations yang saya lakukan pada exercise 5 ini memberikan kejelasan tentang test mana yang bertanggung jawab pada suatu aktivitas. Test yang dibuat akan menjadi lebih mudah dibaca karena adanya pemisahan pada setiap test yang melakukan aktivitas berbeda. \ No newline at end of file diff --git a/functional_tests/base.py b/functional_tests/base.py new file mode 100644 index 0000000000000000000000000000000000000000..0929e71ca0e66fc9c55e4bf812c7cb49e31066d1 --- /dev/null +++ b/functional_tests/base.py @@ -0,0 +1,44 @@ +from selenium import webdriver +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.keys import Keys +from selenium.common.exceptions import WebDriverException +import time + +MAX_WAIT = 10 + +class FunctionalTest(StaticLiveServerTestCase): + + def setUp(self): + options = Options() + options.add_argument('--dns-prefetch-disable') + options.add_argument('--no-sandbox') + options.add_argument('--headless') + options.add_argument('disable-gpu') + self.browser = webdriver.Chrome(executable_path="./chromedriver", options=options) + + def tearDown(self): + self.browser.quit() + + def wait_for_row_in_list_table(self, row_text): + start_time = time.time() + while True: + try: + table = self.browser.find_element_by_id('id_list_table') + rows = table.find_elements_by_tag_name('tr') + self.assertIn(row_text, [row.text for row in rows]) + return + except (AssertionError, WebDriverException) as e: + if time.time() - start_time > MAX_WAIT: + raise e + time.sleep(0.5) + + def wait_for(self, fn): + start_time = time.time() + while True: + try: + return fn() + except (AssertionError, WebDriverException) as e: + if time.time() - start_time > MAX_WAIT: + raise e + time.sleep(0.5) diff --git a/functional_tests/test_layout_and_styling.py b/functional_tests/test_layout_and_styling.py new file mode 100644 index 0000000000000000000000000000000000000000..f64fd1b827b25f2b0737caf00fe12e45c8bd43c8 --- /dev/null +++ b/functional_tests/test_layout_and_styling.py @@ -0,0 +1,44 @@ +from selenium import webdriver +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from selenium.webdriver.common.keys import Keys +from .base import FunctionalTest + +class LayoutAndStylingTest(FunctionalTest): + + def test_layout_and_styling(self): + # Edith goes to the home page + self.browser.get(self.live_server_url) + self.browser.set_window_size(1024, 768) + + # She notices the input box is nicely centered + inputbox = self.browser.find_element_by_id('id_new_item') + self.assertAlmostEqual( + inputbox.location['x'] + inputbox.size['width'] / 2, + 512, + delta=10 + ) + inputbox.send_keys('testing') + inputbox.send_keys(Keys.ENTER) + self.wait_for_row_in_list_table('1: testing') + inputbox = self.browser.find_element_by_id('id_new_item') + self.assertAlmostEqual( + inputbox.location['x'] + inputbox.size['width'] / 2, + 512, + delta=17 + ) + + def check_for_row_in_list_table_none(self): + h1_elem = self.browser.find_element_by_id('comment') + rows = h1_elem.find_elements_by_tag_name('h1') + self.assertIn("sibuk tapi santai", [row.text for row in rows]) + + def test_body_contains_name_in_h1(self): + self.browser.get(self.live_server_url) + h1_elem = self.browser.find_element_by_tag_name('h1') + self.assertIn('Aviliani Pramestya', h1_elem.text) + + def test_body_contains_npm_in_h1(self): + self.browser.get(self.live_server_url) + h1_elem = self.browser.find_element_by_tag_name('h2') + self.assertIn('1606829402', h1_elem.text) + diff --git a/functional_tests/test_list_item_validation.py b/functional_tests/test_list_item_validation.py new file mode 100644 index 0000000000000000000000000000000000000000..ccc0d60b4cb796642cec84408cab333fa552c3a1 --- /dev/null +++ b/functional_tests/test_list_item_validation.py @@ -0,0 +1,39 @@ +from selenium.webdriver.common.keys import Keys +from unittest import skip +from .base import FunctionalTest + +class ItemValidationTest(FunctionalTest): + + def test_cannot_add_empty_list_items(self): + # Edith goes to the home page and accidentally tries to submit + # an empty list item. She hits Enter on the empty input box + self.browser.get(self.live_server_url) + self.browser.find_element_by_id('id_new_item').send_keys(Keys.ENTER) + + + # The home page refreshes, and there is an error message saying + # that list items cannot be blank + self.wait_for(lambda: self.assertEqual( + self.browser.find_element_by_css_selector('.has-error').text, + "Please enter your to do list" + )) + + # She tries again with some text for the item, which now works + self.browser.find_element_by_id('id_new_item').send_keys('Buy milk') + self.browser.find_element_by_id('id_new_item').send_keys(Keys.ENTER) + self.wait_for_row_in_list_table('1: Buy milk') + + # Perversely, she now decides to submit a second blank list item + self.browser.find_element_by_id('id_new_item').send_keys(Keys.ENTER) + + # She receives a similar warning on the list page + self.wait_for(lambda: self.assertEqual( + self.browser.find_element_by_css_selector('.has-error').text, + "Please enter your to do list" + )) + + # And she can correct it by filling some text in + self.browser.find_element_by_id('id_new_item').send_keys('Make tea') + self.browser.find_element_by_id('id_new_item').send_keys(Keys.ENTER) + self.wait_for_row_in_list_table('1: Buy milk') + self.wait_for_row_in_list_table('2: Make tea') diff --git a/functional_tests/tests.py b/functional_tests/test_simple_list_creation.py similarity index 51% rename from functional_tests/tests.py rename to functional_tests/test_simple_list_creation.py index b7a73620f4aaf6b05689ed5c70887d3e18ac9d75..b7a16b27a3b7dca855b3b84a7f4a41e2bf1ca3ad 100644 --- a/functional_tests/tests.py +++ b/functional_tests/test_simple_list_creation.py @@ -1,39 +1,11 @@ from selenium import webdriver from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.keys import Keys -from selenium.common.exceptions import WebDriverException -import time -import unittest +from .base import FunctionalTest -MAX_WAIT = 10 -class NewVisitorTest(StaticLiveServerTestCase): +class NewVisitorTest(FunctionalTest): - def setUp(self): - options = Options() - options.add_argument('--dns-prefetch-disable') - options.add_argument('--no-sandbox') - options.add_argument('--headless') - options.add_argument('disable-gpu') - self.browser = webdriver.Chrome(executable_path="./chromedriver", options=options) - - def tearDown(self): - self.browser.quit() - - def wait_for_row_in_list_table(self, row_text): - start_time = time.time() - while True: - try: - table = self.browser.find_element_by_id('id_list_table') - rows = table.find_elements_by_tag_name('tr') - self.assertIn(row_text, [row.text for row in rows]) - return - except (AssertionError, WebDriverException) as e: - if time.time() - start_time > MAX_WAIT: - raise e - time.sleep(0.5) - def test_can_start_a_list_for_one_user(self): self.browser.get(self.live_server_url) @@ -54,6 +26,7 @@ class NewVisitorTest(StaticLiveServerTestCase): self.wait_for_row_in_list_table('1: Buy peacock feathers') self.wait_for_row_in_list_table('2: Use peacock feathers to make a fly') + def test_multiple_users_can_start_lists_at_different_urls(self): # Edith starts a new to-do list self.browser.get(self.live_server_url) @@ -95,43 +68,3 @@ class NewVisitorTest(StaticLiveServerTestCase): page_text = self.browser.find_element_by_tag_name('body').text self.assertNotIn('Buy peacock feathers', page_text) self.assertIn('Buy milk', page_text) - - - def test_layout_and_styling(self): - # Edith goes to the home page - self.browser.get(self.live_server_url) - self.browser.set_window_size(1024, 768) - - # She notices the input box is nicely centered - inputbox = self.browser.find_element_by_id('id_new_item') - self.assertAlmostEqual( - inputbox.location['x'] + inputbox.size['width'] / 2, - 512, - delta=10 - ) - inputbox.send_keys('testing') - inputbox.send_keys(Keys.ENTER) - self.wait_for_row_in_list_table('1: testing') - inputbox = self.browser.find_element_by_id('id_new_item') - self.assertAlmostEqual( - inputbox.location['x'] + inputbox.size['width'] / 2, - 512, - delta=17 - ) - - - def check_for_row_in_list_table_none(self): - h1_elem = self.browser.find_element_by_id('comment') - rows = h1_elem.find_elements_by_tag_name('h1') - self.assertIn("sibuk tapi santai", [row.text for row in rows]) - - def test_body_contains_name_in_h1(self): - self.browser.get(self.live_server_url) - h1_elem = self.browser.find_element_by_tag_name('h1') - assert h1_elem.text == 'Aviliani Pramestya' - - def test_body_contains_npm_in_h1(self): - self.browser.get(self.live_server_url) - h1_elem = self.browser.find_element_by_tag_name('h2') - assert h1_elem.text == '1606829402' - diff --git a/lists/templates/base.html b/lists/templates/base.html index e59c24d596eaa0181135d253945c3ee939e410de..2bf1850a4edff889eaa1e73a113f027214d723bf 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -30,6 +30,11 @@
{% csrf_token %} + {% if error %} +
+ Please enter your to do list +
+ {% endif %}
​ @@ -48,7 +53,7 @@ {% endblock %} ​ ​ - + \ No newline at end of file diff --git a/lists/tests/__init__.py b/lists/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lists/tests/test_models.py b/lists/tests/test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..87348862b171bdddbcc5c61b99c7dd4fab4fdd80 --- /dev/null +++ b/lists/tests/test_models.py @@ -0,0 +1,31 @@ +from django.test import TestCase +from lists.models import Item, List + +class ListAndItemModelsTest(TestCase): + + def test_saving_and_retrieving_items(self): + list_ = List() + list_.save() + + first_item = Item() + first_item.text = 'The first (ever) list item' + first_item.list = list_ + first_item.save() + + second_item = Item() + second_item.text = 'Item the second' + second_item.list = list_ + second_item.save() + + saved_list = List.objects.first() + self.assertEqual(saved_list, list_) + + saved_items = Item.objects.all() + self.assertEqual(saved_items.count(), 2) + + first_saved_item = saved_items[0] + second_saved_item = saved_items[1] + self.assertEqual(first_saved_item.text, 'The first (ever) list item') + self.assertEqual(first_saved_item.list, list_) + self.assertEqual(second_saved_item.text, 'Item the second') + self.assertEqual(second_saved_item.list, list_) diff --git a/lists/tests.py b/lists/tests/test_views.py similarity index 82% rename from lists/tests.py rename to lists/tests/test_views.py index fc9972c11b57a4b57a2f6011c673e10e971c0493..ee5026240c1efe7ddc935e7d4b732ff126eeae50 100644 --- a/lists/tests.py +++ b/lists/tests/test_views.py @@ -1,6 +1,7 @@ from django.urls import resolve from django.test import TestCase from django.http import HttpRequest +from django.template.loader import render_to_string from lists.views import home_page from lists.models import Item, List @@ -50,36 +51,6 @@ class HomePageTest(TestCase): self.assertIn('1606829402', html) -class ListAndItemModelsTest(TestCase): - - def test_saving_and_retrieving_items(self): - list_ = List() - list_.save() - - first_item = Item() - first_item.text = 'The first (ever) list item' - first_item.list = list_ - first_item.save() - - second_item = Item() - second_item.text = 'Item the second' - second_item.list = list_ - second_item.save() - - saved_list = List.objects.first() - self.assertEqual(saved_list, list_) - - saved_items = Item.objects.all() - self.assertEqual(saved_items.count(), 2) - - first_saved_item = saved_items[0] - second_saved_item = saved_items[1] - self.assertEqual(first_saved_item.text, 'The first (ever) list item') - self.assertEqual(first_saved_item.list, list_) - self.assertEqual(second_saved_item.text, 'Item the second') - self.assertEqual(second_saved_item.list, list_) - - class ListViewTest(TestCase): def test_uses_list_template(self): list_ = List.objects.create() @@ -107,6 +78,14 @@ class ListViewTest(TestCase): response = self.client.get(f'/lists/{correct_list.id}/') self.assertEqual(response.context['list'], correct_list) + def test_POST_empty_list_item_on_list_page(self): + list_ = List.objects.create() + response = self.client.post( + f'/lists/{list_.id}/add_item', + data = {'item_text': ''} + ) + self.assertTemplateUsed(response, 'list.html') + self.assertContains(response, "Please enter your to do list") class NewListTest(TestCase): @@ -120,6 +99,13 @@ class NewListTest(TestCase): response = self.client.post('/lists/new', data={'item_text': 'A new list item'}) new_list = List.objects.first() self.assertRedirects(response, f'/lists/{new_list.id}/') + + def test_POST_empty_list_item_on_homepage(self): + list_ = List.objects.create() + response = self.client.post('/lists/new', data={'item_text': ''}) + self.assertTemplateUsed(response, 'index.html') + self.assertContains(response, "Please enter your to do list") + class NewItemTest(TestCase): diff --git a/lists/views.py b/lists/views.py index 774ce7e68f9ffe3d647271fc688a129b42d7cb4e..d4c61b1bf2c87e241fe49c6aac51a9f127474f8a 100644 --- a/lists/views.py +++ b/lists/views.py @@ -3,21 +3,27 @@ from django.http import HttpResponse from lists.models import Item, List # Create your views here. -def home_page(request): +def home_page(request, error=''): # items = Item.objects.all() - return render(request, 'index.html') + return render(request, 'index.html', {'error': error}) -def view_list(request, list_id): +def view_list(request, list_id, error=''): list_ = List.objects.get(id=list_id) items = Item.objects.filter(list=list_) - return render(request, 'list.html', {'list': list_,'items': items,}) + return render(request, 'list.html', {'list': list_,'items': items, 'error': error}) def new_list(request): + if (len(request.POST['item_text']) == 0): + error = "You can't have an empty list item" + return home_page(request, error) list_ = List.objects.create() Item.objects.create(text=request.POST['item_text'], list=list_) return redirect(f'/lists/{list_.id}/') def add_item(request, list_id): + if (len(request.POST['item_text']) == 0): + error = "You can't have an empty list item" + return view_list(request, list_id, error) list_ = List.objects.get(id=list_id) Item.objects.create(text=request.POST['item_text'], list=list_) return redirect(f'/lists/{list_.id}/') \ No newline at end of file