From fb95c12df5c2048049e4922b3262d11ea17ca914 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Sun, 21 Oct 2018 15:48:26 +0100 Subject: [PATCH 1/6] Add Django 2.1 to TravisCI build --- .travis.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index ff10b04..7549b49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,39 @@ language: python +dist: xenial +sudo: true +services: + - postgresql python: - "2.7" - "3.4" - "3.5" - "3.6" + - "3.7" env: - DJANGO='django>=1.11,<2' - DJANGO='django>=2,<2.1' + - DJANGO='django~=2.1.0' - DJANGO='--pre django' matrix: exclude: - - python: "2.7" - env: DJANGO='django>=2,<2.1' - - python: "3.4" - env: DJANGO='--pre django' + - python: "2.7" + env: DJANGO='--pre django' + - python: "2.7" + env: DJANGO='django~=2.1.0' + - python: "2.7" + env: DJANGO='django>=2,<2.1' + - python: "3.4" + env: DJANGO='django~=2.1.0' + - python: "3.4" + env: DJANGO='--pre django' + - python: "3.7" + env: DJANGO='django>=1.11,<2' allow_failures: - env: DJANGO='--pre django' fast_finish: true -install: +before_script: - psql -c 'CREATE DATABASE orderable' -U postgres; +install: - pip install $DJANGO - pip install -r requirements.txt - pip install -e . From 063e78da5b49db56ca21000d43b4583eddea96a9 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Sat, 27 Oct 2018 11:22:10 +0100 Subject: [PATCH 2/6] Update flake8 to fully support Python 3.7 --- orderable/tests/run.py | 2 +- requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/orderable/tests/run.py b/orderable/tests/run.py index ddfad38..774f800 100644 --- a/orderable/tests/run.py +++ b/orderable/tests/run.py @@ -1,5 +1,5 @@ -import sys from argparse import ArgumentParser +import sys import django from django.conf import settings diff --git a/requirements.txt b/requirements.txt index f643e9f..84d043f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ coverage==3.7.1 -flake8==2.2.5 -flake8-import-order==0.5.3 +flake8==3.6.0 +flake8-import-order==0.18 hypothesis==2.0.0 psycopg2==2.7.4 From c79db3165594da528eb7a5e504d048e6518b38ea Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Sat, 27 Oct 2018 12:11:18 +0100 Subject: [PATCH 3/6] Add selenium test for admin list page Part of #30 --- Makefile | 1 + bdd/bdd/__init__.py | 0 bdd/bdd/admin.py | 7 ++++ bdd/bdd/migrations/0001_initial.py | 25 +++++++++++++ bdd/bdd/migrations/__init__.py | 0 bdd/bdd/models.py | 5 +++ bdd/bdd/settings.py | 57 ++++++++++++++++++++++++++++++ bdd/bdd/urls.py | 7 ++++ bdd/manage.py | 15 ++++++++ features/browser.py | 9 +++++ features/environment.py | 11 ++++++ features/order-list.feature | 15 ++++++++ features/pages.py | 22 ++++++++++++ features/steps/steps.py | 43 ++++++++++++++++++++++ requirements.txt | 4 +++ setup.cfg | 4 ++- 16 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 bdd/bdd/__init__.py create mode 100644 bdd/bdd/admin.py create mode 100644 bdd/bdd/migrations/0001_initial.py create mode 100644 bdd/bdd/migrations/__init__.py create mode 100644 bdd/bdd/models.py create mode 100644 bdd/bdd/settings.py create mode 100644 bdd/bdd/urls.py create mode 100755 bdd/manage.py create mode 100644 features/browser.py create mode 100644 features/environment.py create mode 100644 features/order-list.feature create mode 100644 features/pages.py create mode 100644 features/steps/steps.py diff --git a/Makefile b/Makefile index 7ea8161..b8fd774 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ help: test: @coverage run orderable/tests/run.py @coverage report -m + @bdd/manage.py behave @flake8 release: diff --git a/bdd/bdd/__init__.py b/bdd/bdd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bdd/bdd/admin.py b/bdd/bdd/admin.py new file mode 100644 index 0000000..b3cffdb --- /dev/null +++ b/bdd/bdd/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from orderable.admin import OrderableAdmin +from .models import Item + + +admin.site.register(Item, OrderableAdmin) diff --git a/bdd/bdd/migrations/0001_initial.py b/bdd/bdd/migrations/0001_initial.py new file mode 100644 index 0000000..9196ace --- /dev/null +++ b/bdd/bdd/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.2 on 2018-10-26 21:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sort_order', models.IntegerField(blank=True, db_index=True)), + ], + options={ + 'ordering': ['sort_order'], + 'abstract': False, + }, + ), + ] diff --git a/bdd/bdd/migrations/__init__.py b/bdd/bdd/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bdd/bdd/models.py b/bdd/bdd/models.py new file mode 100644 index 0000000..3868065 --- /dev/null +++ b/bdd/bdd/models.py @@ -0,0 +1,5 @@ +from orderable.models import Orderable + + +class Item(Orderable): + pass diff --git a/bdd/bdd/settings.py b/bdd/bdd/settings.py new file mode 100644 index 0000000..172b597 --- /dev/null +++ b/bdd/bdd/settings.py @@ -0,0 +1,57 @@ +import os + + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +SECRET_KEY = 'r+n-oywnjf7&b*#(!@zx-wa4j=4_yy7a%j-lep&q8nx6^=lwf5' +ROOT_URLCONF = 'bdd.urls' +STATIC_URL = '/static/' +DATABASES = {'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'orderable', + 'HOST': 'localhost' +}} + +INSTALLED_APPS = [ + # Project. + 'bdd', + + # 3rd party. + 'behave_django', + 'orderable', + + # Django. + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] diff --git a/bdd/bdd/urls.py b/bdd/bdd/urls.py new file mode 100644 index 0000000..09e4254 --- /dev/null +++ b/bdd/bdd/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from django.contrib import admin + + +urlpatterns = [ + url('^admin/', admin.site.urls), +] diff --git a/bdd/manage.py b/bdd/manage.py new file mode 100755 index 0000000..7587891 --- /dev/null +++ b/bdd/manage.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bdd.settings') + try: + from django.core.management import execute_from_command_line + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + execute_from_command_line(sys.argv) diff --git a/features/browser.py b/features/browser.py new file mode 100644 index 0000000..016f417 --- /dev/null +++ b/features/browser.py @@ -0,0 +1,9 @@ +from selenium import webdriver + + +class Browser(object): + driver = webdriver.Chrome() + driver.implicitly_wait(5) + + def close(context): + context.driver.close() diff --git a/features/environment.py b/features/environment.py new file mode 100644 index 0000000..c31e283 --- /dev/null +++ b/features/environment.py @@ -0,0 +1,11 @@ +from .browser import Browser +from .pages import ItemListPage + + +def before_all(context): + context.browser = Browser() + context.item_list_page = ItemListPage() + + +def after_all(context): + context.browser.close() diff --git a/features/order-list.feature b/features/order-list.feature new file mode 100644 index 0000000..90a5828 --- /dev/null +++ b/features/order-list.feature @@ -0,0 +1,15 @@ +Feature: Ordering on the list page + + Scenario: Move an item to the end of the list + Given the following items: + | pk | + | 1 | + | 2 | + | 3 | + And we are on the item list page + When item 1 is moved to position 3 + Then the items should be ordered thus: + | pk | + | 2 | + | 3 | + | 1 | diff --git a/features/pages.py b/features/pages.py new file mode 100644 index 0000000..54ba3ae --- /dev/null +++ b/features/pages.py @@ -0,0 +1,22 @@ +from browser import Browser +from selenium.webdriver.common.action_chains import ActionChains + + +class ItemListPage(Browser): + def move_item(self, source, destination): + + template = '#neworder-{} .ui-sortable-handle' + find = self.driver.find_element_by_css_selector + offset = 2 if destination > source else -2 + ( + ActionChains(self.driver) + .click_and_hold(find(template.format(source))) + .move_to_element(find(template.format(destination))) + .move_by_offset(0, offset) + .release() + .perform() + ) + + def open(self, context): + url = context.get_url('admin:bdd_item_changelist') + self.driver.get(url) diff --git a/features/steps/steps.py b/features/steps/steps.py new file mode 100644 index 0000000..d9c7553 --- /dev/null +++ b/features/steps/steps.py @@ -0,0 +1,43 @@ +import time + +from behave import given, then, when +from django.contrib.auth.models import User +from seleniumlogin import force_login + +from bdd.models import Item + + +@given(u'the following items') +def set_up_items(context): + Item.objects.bulk_create([ + Item(pk=row['pk'], sort_order=i) + for i, row + in enumerate(context.table) + ]) + + +@given(u'we are on the item list page') +def go_to_admin_page(context): + user = User.objects.create_superuser( + email='user@example.com', + password='password', + username='myuser', + ) + force_login(user, context.browser.driver, context.test.live_server_url) + context.item_list_page.open(context) + + +@when(u'item {initial_position:d} is moved to position {new_position:d}') +def move_items(context, initial_position, new_position): + context.item_list_page.move_item(initial_position, new_position) + time.sleep(.1) + + +@then(u'the items should be ordered thus') +def check_item_order(context): + items = list(Item.objects.values_list('pk', flat=True)) + expected = [] + for row in context.table: + expected.append(int(row['pk'])) + + assert items == expected, (items, expected) diff --git a/requirements.txt b/requirements.txt index 84d043f..42f7248 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,9 @@ +behave==1.2.6 +behave_django==1.1.0 coverage==3.7.1 +django-selenium-login==1.0.2 flake8==3.6.0 flake8-import-order==0.18 hypothesis==2.0.0 psycopg2==2.7.4 +selenium==3.14.1 diff --git a/setup.cfg b/setup.cfg index d829475..426ffef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,9 @@ [wheel] universal = 1 [flake8] -application-import-names = orderable +application-import-names = orderable, bdd +exclude = + bdd/bdd/migrations import-order-style = google max-complexity = 10 max-line-length = 90 From 7501bbc4a2816c2dacdaa6af50379ef4cab3f8ca Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Sat, 27 Oct 2018 12:27:28 +0100 Subject: [PATCH 4/6] Run the selenium tests in headless mode --- features/browser.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/features/browser.py b/features/browser.py index 016f417..e74d617 100644 --- a/features/browser.py +++ b/features/browser.py @@ -1,8 +1,13 @@ from selenium import webdriver +chrome_options = webdriver.chrome.options.Options() +chrome_options.add_argument("--headless") +chrome_options.add_argument("--window-size=1024x768") + + class Browser(object): - driver = webdriver.Chrome() + driver = webdriver.Chrome(chrome_options=chrome_options) driver.implicitly_wait(5) def close(context): From 68ed554c8dcb332198f5263a8c286e4368cbccc0 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Sat, 27 Oct 2018 12:35:19 +0100 Subject: [PATCH 5/6] Add chrome addon, and install chromedriver --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7549b49..3e68410 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,11 @@ language: python dist: xenial sudo: true +addons: + chrome: stable + apt: + packages: + - chromium-chromedriver services: - postgresql python: @@ -32,6 +37,8 @@ matrix: - env: DJANGO='--pre django' fast_finish: true before_script: + # Add link to ChromeDriver in PATH + - ln --symbolic /usr/lib/chromium-browser/chromedriver "${HOME}/bin/chromedriver" - psql -c 'CREATE DATABASE orderable' -U postgres; install: - pip install $DJANGO From f0d704d690841c5c868553b2dfc6bbbc99e4bc35 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Sat, 27 Oct 2018 13:02:56 +0100 Subject: [PATCH 6/6] Add second behaviour test --- features/order-list.feature | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/features/order-list.feature b/features/order-list.feature index 90a5828..128ed7d 100644 --- a/features/order-list.feature +++ b/features/order-list.feature @@ -13,3 +13,17 @@ Feature: Ordering on the list page | 2 | | 3 | | 1 | + + Scenario: Move an item to the top of the list + Given the following items: + | pk | + | 1 | + | 2 | + | 3 | + And we are on the item list page + When item 3 is moved to position 1 + Then the items should be ordered thus: + | pk | + | 3 | + | 1 | + | 2 |