Покрыть тестами - заполнение курсов.

remotes/origin/feature/testing_courses_30-01-19
gzbender 7 years ago
parent 14ddc2b28c
commit 15954fc66a
  1. 85
      apps/course/tests/test_views.py
  2. 44
      project/tests/__init__.py
  3. 38
      project/utils/selenium_utils.py
  4. 2
      requirements.txt
  5. 71
      web/src/components/CourseRedactor.vue

@ -2,14 +2,19 @@ from datetime import timedelta
from random import randint
from selenium import webdriver
from factory.faker import Faker
from django.utils import timezone
from django.test import TestCase
from django.shortcuts import reverse
from django.conf import settings
from django.utils.text import slugify
from unidecode import unidecode
from project.tests.factories import *
from project.tests import SeleniumTestCase
from project.utils.selenium_utils import SeleniumExtensions as SE
'''
class CoursesTestCase(TestCase):
@classmethod
@ -18,6 +23,11 @@ class CoursesTestCase(TestCase):
create_batch_unique(CourseFactory, status=Course.STATUS_CHOICES[:3], price=[0, 1000],
age=Course.AGE_CHOICES[:2], deferred_start_at=[None, timezone.now() + timedelta(days=randint(5, 15))])
@classmethod
def tearDownClass(cls):
print('teardown CoursesTestCase')
super().tearDownClass()
def test_courses_url_accessible(self):
print('get ', reverse('courses'))
resp = self.client.get(reverse('courses'))
@ -35,16 +45,79 @@ class CoursesTestCase(TestCase):
print('get ', reverse('course_edit', args=[course.id]))
resp = self.client.get(reverse('course_edit', args=[course.id]))
self.assertEqual(resp.status_code, 200)
'''
class CourseEditTestCase(TestCase):
class CourseEditTestCase(SeleniumTestCase):
@classmethod
def setUpTestData(cls):
cls.driver = webdriver.Chrome()
create_admin()
create_batch_unique(CourseFactory, status=Course.STATUS_CHOICES[:3], price=[0, 1000],
age=Course.AGE_CHOICES[:2], deferred_start_at=[None, timezone.now() + timedelta(days=randint(5, 15))])
UserFactory.create_batch(5, role=User.AUTHOR_ROLE)
# create_batch_unique(CourseFactory, status=Course.STATUS_CHOICES[:3], price=[0, 1000],
# age=Course.AGE_CHOICES[:2], deferred_start_at=[None, timezone.now() + timedelta(days=randint(5, 15))])
@classmethod
def tearDownClass(cls):
print('teardown CourseEditTestCase')
super().tearDownClass()
def test_course_edit(self):
print('Course.objects.all().count()', Course.objects.all().count())
print('go to google')
self.driver.get('http://www.google.com')
return
user = User.objects.filter(role=User.AUTHOR_ROLE).first()
self.client.force_login(user)
url = self.get_url(reverse('course_create'))
print('url', url)
self.driver.get(url)
# visible always elements
title_el = self.wait_elem_name('course-title')
slug_el = self.wait_elem_name('course-slug')
category_el = self.wait_elem_name('course-category')
is_paid_no_el = self.wait_elem_name('course-is-paid-no')
is_paid_yes_el = self.wait_elem_name('course-is-paid-yes')
is_paid_input_el = self.wait_elem_name('course-is-paid-input')
age_el = self.wait_elem_name('course-age')
is_featured_el = self.wait_elem_name('course-is-featured')
is_deferred_no_el = self.wait_elem_name('course-is-deferred-no')
is_deferred_yes_el = self.wait_elem_name('course-is-deferred-yes')
is_deferred_input_el = self.wait_elem_name('course-is-deferred-input')
content_btn_el = self.wait_elem_name('course-content-btn')
lessons_btn_el = self.wait_elem_name('course-lessons-btn')
content_el = self.wait_elem_name('course-content')
title = Faker('sentence', nb_words=6)
title_el.send_keys(title)
slug = slugify(unidecode(title[:90]))
print('title', title, 'slug', slug)
self.assertEqual(slug_el.text, slug)
print("is_paid_input_el.get_attribute('checked')", is_paid_input_el.get_attribute('checked'))
self.assertFalse(is_paid_input_el.get_attribute('checked'))
self.assertRaises(callable=lambda: self.driver.find_element_by_name('course-price'))
self.assertRaises(callable=lambda: self.driver.find_element_by_name('course-old-price'))
is_paid_yes_el.click()
try:
price_el = self.wait_elem_name('course-price')
old_price_el = self.wait_elem_name('course-old-price')
except:
self.fail('Price and old price elements not shown')
self.assertFalse(is_deferred_input_el.get_attribute('checked'))
self.assertRaises(callable=lambda: self.driver.find_element_by_name('course-date'))
self.assertRaises(callable=lambda: self.driver.find_element_by_name('course-time'))
is_deferred_yes_el.click()
try:
date_el = self.wait_elem_name('course-date')
time_el = self.wait_elem_name('course-time')
except:
self.fail('Date and time elements not shown')
# lessons_el = self.wait_elem_name('course-lessons')
# add_lesson_el = self.wait_elem_name('course-add-lesson')
# lesson_edit_el = self.wait_elem_name('course-lesson-edit')
# stream_el = self.wait_elem_name('course-stream')

@ -0,0 +1,44 @@
from django.test import TestCase
from selenium import webdriver
from selenium.webdriver.common.by import By
from pyvirtualdisplay import Display
from project.utils.selenium_utils import SeleniumExtensions as SE
from django.conf import settings
class SeleniumTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.display = Display(visible=0, size=(1280, 1024))
cls.display.start()
cls.driver = webdriver.Chrome()
@classmethod
def tearDownClass(cls):
cls.driver.close()
cls.display.stop()
super().tearDownClass()
def get_url(self, url):
return 'http://%s%s' % (settings.MAIN_HOST, url)
def wait_elem_xpath(self, xpath, inside_el=None, wait_time=10):
return SE.wait_elem(self.driver, (By.XPATH, xpath), inside_el, wait_time)
def wait_elems_xpath(self, xpath, inside_el=None, wait_time=10):
return SE.wait_elems(self.driver, (By.XPATH, xpath), inside_el, wait_time)
def wait_elem_css(self, selector, inside_el=None, wait_time=10):
return SE.wait_elem(self.driver, (By.CSS_SELECTOR, selector), inside_el, wait_time)
def wait_elems_css(self, selector, inside_el=None, wait_time=10):
return SE.wait_elems(self.driver, (By.CSS_SELECTOR, selector), inside_el, wait_time)
def wait_elem_name(self, name, inside_el=None, wait_time=10):
return SE.wait_elem(self.driver, (By.NAME, name), inside_el, wait_time)
def wait_elems_name(self, name, inside_el=None, wait_time=10):
return SE.wait_elems(self.driver, (By.NAME, name), inside_el, wait_time)

@ -6,45 +6,41 @@ from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class elem_in(object):
default_find_fn = 'find_element_by_xpath'
def __init__(self, element, xpath, find_fn=None):
class ElementIn(object):
default_find_fn = 'find_element'
def __init__(self, element, locator, find_fn=None):
self.element = element
self.xpath = xpath
self.locator = locator
self.find_fn = self.default_find_fn if find_fn is None else find_fn
def __call__(self, driver):
try:
return getattr(self.element, self.find_fn)(self.xpath)
return getattr(self.element, self.find_fn)(self.locator)
except:
return False
class elems_in(elem_in):
default_find_fn = 'find_elements_by_xpath'
class ElementsIn(ElementIn):
default_find_fn = 'find_elements'
class SeleniumExtensions(object):
@classmethod
def wait_elem(cls, driver, xpath, inside_el=None, wait_time=10):
def wait_elem(cls, driver, locator, inside_el=None, wait_time=10):
if inside_el:
return WebDriverWait(driver, wait_time).until(elem_in(inside_el, xpath))
if xpath[:2] != '//':
raise Exception('XPath in wait_elem must start with //')
return WebDriverWait(driver, wait_time).until(ElementIn(inside_el, locator))
return WebDriverWait(driver, wait_time).until(
EC.presence_of_element_located(
(By.XPATH, xpath)
)
EC.presence_of_element_located(locator)
)
@classmethod
def wait_elems(cls, driver, xpath, inside_el=None, wait_time=10):
def wait_elems(cls, driver, locator, inside_el=None, wait_time=10):
if inside_el:
return WebDriverWait(driver, wait_time).until(elems_in(inside_el, xpath))
if xpath[:2] != '//':
raise Exception('XPath in wait_elems must start with //')
return WebDriverWait(driver, wait_time).until(ElementsIn(inside_el, locator))
return WebDriverWait(driver, wait_time).until(
EC.presence_of_all_elements_located(
(By.XPATH, xpath)
)
EC.presence_of_all_elements_located(locator)
)

@ -38,3 +38,5 @@ drf_dynamic_fields
flower==0.9.2
unidecode
factory-boy==2.11.1
pyvirtualdisplay==0.2.1 # + sudo apt-get install xvfb + sudo apt-get install chromium-chromedriver
# + sudo ln -s /usr/lib/chromium-browser/chromedriver /usr/bin/chromedriver

@ -6,12 +6,13 @@
<div class="info__section">
<div class="courses__item">
<div class="courses__preview">
<img class="courses__pic" :src="course.coverImage || defaultCover" width="300px" />
<img class="courses__pic" name="course-cover" :src="course.coverImage || defaultCover" width="300px" />
<div class="upload" v-if="! course.coverImage">
<div class="upload__title">Загрузить превью</div>
<input type="file" class="upload__file" @change="onCoverImageSelected">
<input type="file" class="upload__file" name="course-cover-upload" @change="onCoverImageSelected">
</div>
<a href="#" title="Удалить превью" class="course-delete-cover" v-if="course.coverImage" @click="removeCover">
<a href="#" title="Удалить превью" class="course-delete-cover" v-if="course.coverImage" @click="removeCover"
name="course-cover-delete">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
@ -20,13 +21,13 @@
<div class="courses__details">
<div class="field-category courses__theme theme field info__field--light" v-if="!live" v-bind:class="{ error: (!$v.live && $v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
<lil-select :value.sync="course.category" :options="categoryOptions"
placeholder="Выберите категорию"/>
placeholder="Выберите категорию" name="course-category"/>
</div>
<div class="courses__old-price" v-if="course.is_paid && course.old_price"><s>{{ course.old_price }}</s></div>
<div class="courses__price" v-if="course.is_paid && course.price">{{ course.price }}</div>
</div>
<div class="courses__title field field" v-bind:class="{ error: ($v.course.title.$dirty || showErrors) && $v.course.title.$invalid }">
<textarea class="field__textarea"
<textarea class="field__textarea" name="course-title"
rows="1"
:title="titles.courseTitle"
v-autosize="course.title"
@ -34,7 +35,7 @@
v-model="course.title"
placeholder="Добавить заголовок"></textarea>
</div>
<div class="courses__content field" style="width: 300px;"
<div class="courses__content field" style="width: 300px;" name="course-short-description"
v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }">
<vue-redactor :value.sync="course.short_description" placeholder="Добавить краткое описание"/>
</div>
@ -46,7 +47,7 @@
<div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ССЫЛКА</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model="course.slug" @input="slugChanged = true">
<input type="text" class="field__input" name="course-slug" v-model="course.slug" @input="slugChanged = true">
</div>
<div class="field__wrap field__wrap--additional">{{ courseFullUrl }}</div>
</div>
@ -55,7 +56,7 @@
v-bind:class="{ error: ($v.course.stream.$dirty || showErrors) && $v.course.stream.$invalid }">
<div class="field__label field__label_gray">ССЫЛКА НА VIMEO</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model="course.stream">
<input type="text" name="course-stream" class="field__input" v-model="course.stream">
</div>
</div>
@ -71,8 +72,8 @@
v-bind:class="{ error: ($v.course.duration.$dirty || showErrors) && $v.course.duration.$invalid }">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap field__wrap__appended">
<input type="text" class="field__input field__input__appended" v-model.number="course.duration"
@input="$v.course.duration.$touch()">
<input type="text" class="field__input field__input__appended" name="course-duration"
v-model.number="course.duration" @input="$v.course.duration.$touch()">
<button disabled class="field__append">{{pluralize(course.duration, ['день', 'дня', 'дней'])}}</button>
</div>
</div>
@ -81,12 +82,14 @@
<div class="field__label field__label_gray">ДОСТУП</div>
<div class="field__wrap">
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="false" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Бесплатный</span>
<input type="radio" :value="false" class="switch__input" v-model="course.is_paid"
name="course-is-paid-input">
<span class="switch__content" name="course-is-paid-no">Бесплатный</span>
</label>
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="true" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Платный</span>
<input type="radio" :value="true" class="switch__input" v-model="course.is_paid"
name="course-is-paid-input">
<span class="switch__content" name="course-is-paid-yes">Платный</span>
</label>
</div>
</div>
@ -94,27 +97,29 @@
<div v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
<div class="field__wrap field__wrap__appended field__wrap__100px">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayPrice">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayPrice"
name="course-price">
<button disabled class="field__append">руб.</button>
</div>
</div>
<div v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ БЕЗ СКИДКИ</div>
<div class="field__wrap field__wrap__appended field__wrap__100px">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayOldPrice">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayOldPrice"
name="course-old-price">
<button disabled class="field__append">руб.</button>
</div>
</div>
<div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ВОЗРАСТ</div>
<div class="field__wrap">
<lil-select :value.sync="course.age" :options="ages" value-key="value"
<lil-select :value.sync="course.age" :options="ages" value-key="value" name="course-age"
placeholder="Выберите возраст"/>
</div>
</div>
<label v-if="me && !live && me.role === ROLE_ADMIN" class="info__switch switch switch_lg">
<input type="checkbox" class="switch__input" v-model="course.is_featured">
<span class="switch__content">Выделить</span>
<span class="switch__content" name="course-is-featured">Выделить</span>
</label>
</div>
<div v-if="!live" class="info__fieldset">
@ -122,12 +127,14 @@
<div class="field__label field__label_gray">ЗАПУСК</div>
<div class="field__wrap">
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="false" class="switch__input" v-model="course.is_deferred">
<span class="switch__content">Мгновенный</span>
<input type="radio" :value="false" class="switch__input" v-model="course.is_deferred"
name="course-is-deferred-input">
<span class="switch__content" name="course-is-deferred-no">Мгновенный</span>
</label>
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="true" class="switch__input" v-model="course.is_deferred">
<span class="switch__content">Отложенный</span>
<input type="radio" :value="true" class="switch__input" v-model="course.is_deferred"
name="course-is-deferred-input">
<span class="switch__content" name="course-is-deferred-yes">Отложенный</span>
</label>
</div>
</div>
@ -135,13 +142,15 @@
<div class="info__field field">
<div class="field__label">ДАТА</div>
<div class="field__wrap">
<vue-datepicker :disabled="disabledDates" input-class="field__input" v-model="course.date" language="ru" format="dd/MM/yyyy"/>
<vue-datepicker :disabled="disabledDates" input-class="field__input" name="course-date"
v-model="course.date" language="ru" format="dd/MM/yyyy"/>
</div>
</div>
<div class="field-time info__field field">
<div class="field__label">ВРЕМЯ</div>
<div class="field__wrap">
<lil-select :value.sync="course.time" value-key="value" :options="timeOptions" placeholder="Выберите время"/>
<lil-select :value.sync="course.time" value-key="value" :options="timeOptions" name="course-time"
placeholder="Выберите время"/>
</div>
</div>
</div>
@ -153,12 +162,12 @@
<div class="section__center center">
<div class="kit" style="margin: 0 auto;">
<div v-if="!live" id="course-redactor__nav" class="kit__nav">
<button class="kit__btn btn btn_lg"
<button class="kit__btn btn btn_lg" name="course-content-btn"
v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }"
type="button" @click="showCourse">Описание
курса
</button>
<button class="kit__btn btn btn_lg"
<button class="kit__btn btn btn_lg" name="course-lessons-btn"
v-bind:class="{ 'btn_stroke': viewSection === 'lessons', 'btn_gray': viewSection !== 'lessons' }"
type="button"
@click="showLessons"
@ -167,7 +176,7 @@
</button>
</div>
<div v-if="viewSection === 'course'" class="kit__body">
<block-content :content.sync="course.content"></block-content>
<block-content :content.sync="course.content" name="course-content"></block-content>
<!--<div class="kit__foot">
<button type="submit" class="kit__submit btn btn_md" v-bind:class="{ loading: courseSaving }">
@ -178,7 +187,8 @@
<div v-if="viewSection === 'lessons'" class="kit__body">
<div class="lessons__title title">Содержание курса</div>
<div v-if="!lessonsLoading" class="lessons__list">
<vue-draggable v-model="lessons" @start="drag=true" @end="onLessonsChanged" :options="{ handle: '.sortable__handle' }">
<vue-draggable v-model="lessons" @start="drag=true" @end="onLessonsChanged" name="course-lessons"
:options="{ handle: '.sortable__handle' }">
<div class="lessons__item" v-for="(lesson, index) in lessons" :key="lesson.id">
<div class="lessons__actions">
<button class="sortable__handle" type="button">
@ -206,7 +216,8 @@
</div>
<div v-if="lessonsLoading">Загрузка...</div>
<div class="lessons__foot">
<button type="button" class="lessons__btn btn btn_md" @click="addLesson">СОЗДАТЬ УРОК</button>
<button type="button" class="lessons__btn btn btn_md" @click="addLesson"
name="course-add-lesson">СОЗДАТЬ УРОК</button>
</div>
</div>
</div>
@ -215,7 +226,7 @@
</form>
<form v-if="viewSection === 'lessons-edit'" @submit.prevent="onLessonSubmit">
<lesson-redactor :$v="$v" :lesson.sync="currentLesson" :saving.sync="lessonSaving" :access-token="accessToken"
v-on:back="goToLessons" />
v-on:back="goToLessons" name="course-lesson-edit" />
</form>
</div>
<div v-else>

Loading…
Cancel
Save