Skip to content

Commit

Permalink
Add Cypress with GitHub CI
Browse files Browse the repository at this point in the history
Add Django database support for e2e tests
Update all tests to use server responses, remove fixtures
Add scripts to manage the database between tests
Modify settings.py to include a database cache
Remove Cypress Django test user setup
Add Cypress pylint configuration
Add parallelization for cypress tests
Reorganize Cypress specs
Add better Cypress run titles

Fix issue submitting email for user that doesn't exist
Fix button type in add student modal, add span around emails
  • Loading branch information
smartspot2 committed Dec 31, 2022
1 parent da4c74e commit 68bc8e7
Show file tree
Hide file tree
Showing 27 changed files with 4,908 additions and 75 deletions.
117 changes: 117 additions & 0 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: Cypress E2E
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
cypress-run-chrome:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
containers: [1, 2]
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: csm_web_dev
ports: ["5432:5432"]
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install npm dependencies
run: |
npm ci
- name: Install Chrome 106
run: |
sudo wget --no-verbose -O /usr/src/google-chrome-stable_current_amd64.deb "http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_106.0.5249.91-1_amd64.deb" && \
sudo dpkg -i /usr/src/google-chrome-stable_current_amd64.deb ; \
sudo apt-get install -f -y && \
sudo rm -f /usr/src/google-chrome-stable_current_amd64.deb
- name: Run Cypress
uses: cypress-io/github-action@v4
with:
record: true
build: npm run build
start: python csm_web/manage.py runserver
wait-on: http://localhost:8000
browser: chrome
parallel: true
group: Tests on Chrome 106
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
DJANGO_ENV: dev
POSTGRES_PASSWORD: postgres
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# pass GitHub token to allow accurately detecting a build vs a re-run build
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# better cypress run titles
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
cypress-run-firefox:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
containers: [1, 2]
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: csm_web_dev
ports: ["5432:5432"]
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install npm dependencies
run: |
npm ci
- name: Install Firefox 106
run: |
sudo wget --no-verbose -O /tmp/firefox.tar.bz2 https://download-installer.cdn.mozilla.net/pub/firefox/releases/106.0.2/linux-x86_64/en-US/firefox-106.0.2.tar.bz2 && \
sudo tar -C /opt -xjf /tmp/firefox.tar.bz2 && \
sudo rm /tmp/firefox.tar.bz2 && \
sudo ln -fs /opt/firefox/firefox /usr/bin/firefox
- name: Run Cypress
uses: cypress-io/github-action@v4
with:
record: true
build: npm run build
start: python csm_web/manage.py runserver
wait-on: http://localhost:8000
browser: firefox
parallel: true
group: Tests on Firefox 106
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
DJANGO_ENV: dev
POSTGRES_PASSWORD: postgres
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# pass GitHub token to allow accurately detecting a build vs a re-run build
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# better cypress run titles
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
cypress/videos/
cypress/screenshots/

# Translations
*.mo
Expand Down
8 changes: 8 additions & 0 deletions csm_web/csm_web/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@
"rest_framework.renderers.BrowsableAPIRenderer"
)

# Cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'db_cache_table',
}
}

# Logging
LOGGING = {
'version': 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,13 @@ export function CoordinatorAddStudentModal({
</div>
</div>
<div className="coordinator-email-input-buttons">
<button className="coordinator-email-input-add" onClick={() => addNewEmail()}>
<button className="coordinator-email-input-add" type="button" onClick={() => addNewEmail()}>
Add email
</button>
{
/* Firefox doesn't support clipboard reads from sites; readText would be undefined */
navigator.clipboard.readText != undefined && (
<button className="coordinator-email-input-add" onClick={() => addEmailsFromClipboard()}>
navigator?.clipboard?.readText !== undefined && (
<button className="coordinator-email-input-add" type="button" onClick={() => addEmailsFromClipboard()}>
Add from clipboard
</button>
)
Expand Down Expand Up @@ -312,7 +312,7 @@ export function CoordinatorAddStudentModal({
>
×
</span>
{email_obj.email}
<span>{email_obj.email}</span>
</div>
<div className="coordinator-email-response-item-left-detail">{conflictDetail}</div>
</div>
Expand Down Expand Up @@ -360,7 +360,7 @@ export function CoordinatorAddStudentModal({
>
×
</span>
{email_obj.email}
<span>{email_obj.email}</span>
</div>
</div>
<div className="coordinator-email-response-item-right">
Expand Down Expand Up @@ -402,7 +402,7 @@ export function CoordinatorAddStudentModal({
>
×
</span>
{email_obj.email}
<span>{email_obj.email}</span>
</div>
<div className="coordinator-email-response-item-right"></div>
</div>
Expand Down
28 changes: 16 additions & 12 deletions csm_web/frontend/src/tests/section/StudentDropper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { act, fireEvent, render, waitFor } from "@testing-library/react";
import React from "react";
import StudentDropper from "../../components/section/StudentDropper";
import * as api from "../../utils/api";
import { testQueryClientWrapper } from "../utils";

// mock modal
jest.mock("../../components/Modal", () => {
Expand All @@ -21,14 +22,16 @@ jest.mock("../../components/Modal", () => {

describe("StudentDropper", () => {
it("should render correctly without interaction", () => {
const reloadSection = jest.fn();
const component = render(<StudentDropper id={1} name="John Doe" reloadSection={reloadSection} />);
const component = render(<StudentDropper id={1} sectionId={2} name="Test Student" />, {
wrapper: testQueryClientWrapper
});
expect(component.asFragment()).toMatchSnapshot();
});

it("should render modal correctly after clicking x", () => {
const reloadSection = jest.fn();
const component = render(<StudentDropper id={1} name="Test Student" reloadSection={reloadSection} />);
const component = render(<StudentDropper id={1} sectionId={2} name="Test Student" />, {
wrapper: testQueryClientWrapper
});

act(() => {
// click drop button to bring up modal
Expand All @@ -39,8 +42,9 @@ describe("StudentDropper", () => {
});

it("should close modal correctly", () => {
const reloadSection = jest.fn();
const component = render(<StudentDropper id={1} name="Test Student" reloadSection={reloadSection} />);
const component = render(<StudentDropper id={1} sectionId={2} name="Test Student" />, {
wrapper: testQueryClientWrapper
});

act(() => {
// click drop button to bring up modal
Expand Down Expand Up @@ -68,8 +72,9 @@ describe("StudentDropper", () => {
return null as any;
});

const reloadSection = jest.fn();
const component = render(<StudentDropper id={1} name="Test Student" reloadSection={reloadSection} />);
const component = render(<StudentDropper id={1} sectionId={2} name="Test Student" />, {
wrapper: testQueryClientWrapper
});

// click drop button to bring up modal
act(() => {
Expand Down Expand Up @@ -108,7 +113,6 @@ describe("StudentDropper", () => {
// wait for fetch to finish
waitFor(() => {
expect(spyFetchWithMethod).toHaveBeenCalled();
expect(reloadSection).toHaveBeenCalled();
});
});

Expand All @@ -125,8 +129,9 @@ describe("StudentDropper", () => {
return null as any;
});

const reloadSection = jest.fn();
const component = render(<StudentDropper id={1} name="Test Student" reloadSection={reloadSection} />);
const component = render(<StudentDropper id={1} sectionId={2} name="Test Student" />, {
wrapper: testQueryClientWrapper
});

// click drop button to bring up modal
act(() => {
Expand Down Expand Up @@ -179,7 +184,6 @@ describe("StudentDropper", () => {
// wait for fetch to finish
waitFor(() => {
expect(spyFetchWithMethod).toHaveBeenCalled();
expect(reloadSection).toHaveBeenCalled();
});
});
});
17 changes: 17 additions & 0 deletions csm_web/frontend/src/tests/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export const testQueryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false
}
}
});

/**
* Wrap an element with a query client provider, so that react-query functions correctly.
*/
export const testQueryClientWrapper = ({ children }: { children: React.ReactElement }) => (
<QueryClientProvider client={testQueryClient}>{children}</QueryClientProvider>
);
25 changes: 25 additions & 0 deletions csm_web/scheduler/management/commands/createtestuser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.core.management import BaseCommand
from scheduler.models import User


class Command(BaseCommand):
help = "Populates database with a single test user for testing."

def add_arguments(self, parser):
parser.add_argument(
"--silent", action="store_true", help="no stdout during execution"
)

def handle(self, *args, **options):
demo_user = User.objects.create(
username="demo_user",
email="[email protected]",
first_name="Demo",
last_name="User",
)
demo_user.is_staff = True
demo_user.is_superuser = True
demo_user.set_password("pass")
demo_user.save()
if "silent" not in options or not options["silent"]:
print("Created demo user with username 'demo_user' and password 'pass'")
4 changes: 3 additions & 1 deletion csm_web/scheduler/views/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,9 @@ class BanAction:
)
elif student_queryset.count() == 0:
# check if the user can actually enroll in the section
student_user = User.objects.get(email=email)
student_user, _ = User.objects.get_or_create(
username=email.split('@')[0], email=email
)
if (
student_user.id not in course_coords
and student_user.can_enroll_in_course(section.mentor.course, bypass_enrollment_time=True)
Expand Down
8 changes: 8 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "cypress";

export default defineConfig({
e2e: {
projectId: "ar111y",
baseUrl: "http://localhost:8000"
}
});
3 changes: 3 additions & 0 deletions cypress/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["plugin:cypress/recommended"]
}
2 changes: 2 additions & 0 deletions cypress/db/.pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tool.pylint.main]
load-plugins = ["pylint_django"]
Loading

0 comments on commit 68bc8e7

Please sign in to comment.