From 07c3a70e8695d7de03f0bb4827a8e959b6c1c759 Mon Sep 17 00:00:00 2001 From: Matt Mayfield Date: Tue, 1 Oct 2024 00:50:57 -0400 Subject: [PATCH] Update docs, scripts, and add two project settings tests --- .github/workflows/node.js.yml | 57 +++++++- README.md | 44 ++++++- cypress.config.ts | 34 +++-- cypress/e2e/admin/project_settings.cy.ts | 122 ++++++++++++++++++ cypress/support/commands.ts | 13 +- cypress/support/e2e.ts | 68 +++++++++- package.json | 13 +- src/components/datetime/time_picker.vue | 4 +- .../project_admin/project_admin.vue | 11 ++ .../project_admin/project_settings.vue | 11 +- .../test_datetime/test_time_picker.ts | 10 +- 11 files changed, 351 insertions(+), 36 deletions(-) create mode 100644 cypress/e2e/admin/project_settings.cy.ts diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 09f81c877..a46156f47 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -10,8 +10,7 @@ on: branches: [ develop ] jobs: - build: - + lint: runs-on: ubuntu-latest strategy: @@ -21,17 +20,61 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm install + + - run: npm ci - run: npm run lint + + component-tests: + # Lint is fast. If we wait for it we guarantee a cache hit on dependencies. + needs: lint + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - run: npm ci - run: npm run build - - run: npm test - - run: npx cypress run --component + - run: npm run component - # Starts servers and runs e2e tests once the website is available - - run: npm run e2e + e2e-tests: + # Lint is fast. If we wait for it we guarantee a cache hit on dependencies. + needs: lint + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + with: + submodules: 'true' + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run e2e diff --git a/README.md b/README.md index e373ea54b..9da1118df 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,58 @@ For information about contributing to Autograder.io, see our Follow the [dev stack setup tutorial](https://github.com/eecs-autograder/autograder-full-stack/blob/master/docs/development_setup.md) for the [autograder-full-stack repo](https://github.com/eecs-autograder/autograder-full-stack). ## Dev commands -The unit tests currently support Node.js version 16 (newer versions may work too). +### Run all tests +To run all tests, Node.js version 16 is **required**. Newer versions will not work. You can install this version with [NVM](https://github.com/nvm-sh/nvm/blob/master/README.md) by running: ``` nvm install 16 ``` -To run the unit tests (with coverage): +To run both component tests and e2e tests: ``` npm test ``` +### Unit tests +The unit tests currently **require** Node.js version 16. Newer versions will not work. +See above about installing this version. + +To run the unit tests (with coverage): +``` +npm run component +``` + +### e2e tests +The e2e tests currently require at least Node.js version 16. Newer versions may work. +See above for installing this version. + +To run the e2e tests with a headless browser: +``` +npm run e2e +``` + +To open a full browser from which you can run e2e tests: +``` +npm run e2e:browser +``` + +Both of these npm scripts will start a backend server for the website to interact with and +shut it down once the tests are complete. If you don't want to wait for the app to compile +each time you run the tests, you can alternatively start the backend manually and leave +it running while you run the tests. If you do this, changes to the source code will automatically +cause the app to recompile but it will be much faster than compiling everything from scratch. +``` +# start backend +npm run e2e:serve + +# in a different terminal, or if you ran the serve script as a background process... +npm run e2e:cy:run + +# or to open in a browser... +npm run e2e:cy:open +``` + ## Coding Standards In addition to the items listed here, all source code must follow our [Typescript/Vue coding standards](https://github.com/eecs-autograder/autograder.io/blob/master/coding_standards_typescript_vue.md). diff --git a/cypress.config.ts b/cypress.config.ts index bba2f1fe9..222a63117 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,8 +1,12 @@ import { defineConfig } from "cypress"; import * as child_process from 'child_process'; -// reset db -// make super user +const SUPERUSER_NAME = 'superuser@autograder.io' +const ADMIN_NAME = 'admin@autograder.io' +const STAFF_NAME = 'staff@autograder.io' +const STUDENT_NAME = 'student@autograder.io' + +const CONTAINER_NAME = 'ag-vue-e2e-django' export default defineConfig({ component: { @@ -18,13 +22,16 @@ export default defineConfig({ setup_db }) }, - baseUrl: 'http://localhost:8080' + baseUrl: 'http://localhost:8080', + env: { + superuser: SUPERUSER_NAME, + admin: ADMIN_NAME, + staff: STAFF_NAME, + student: STUDENT_NAME, + }, }, }); -const SUPERUSER_NAME = 'superuser@autograder.io' -const CONTAINER_NAME = 'ag-vue-e2e-django' - const setup_db = () => { let django_code = `import shutil from django.core.cache import cache; @@ -38,9 +45,18 @@ BuildSandboxDockerImageTask.objects.all().delete() shutil.rmtree('/usr/src/app/media_root_dev/', ignore_errors=True) cache.clear() -user = User.objects.get_or_create(username='${SUPERUSER_NAME}')[0] -user.is_superuser = True -user.save() +superuser = User.objects.get_or_create(username='${SUPERUSER_NAME}')[0] +superuser.is_superuser = True +superuser.save() + +admin = User.objects.get_or_create(username='${ADMIN_NAME}')[0] +admin.save() + +staff = User.objects.get_or_create(username='${STAFF_NAME}')[0] +staff.save() + +student = User.objects.get_or_create(username='${STUDENT_NAME}')[0] +student.save() `; return run_in_django_shell(django_code); } diff --git a/cypress/e2e/admin/project_settings.cy.ts b/cypress/e2e/admin/project_settings.cy.ts new file mode 100644 index 000000000..1064a4d51 --- /dev/null +++ b/cypress/e2e/admin/project_settings.cy.ts @@ -0,0 +1,122 @@ +const username = Cypress.env('admin') +const course_name = 'Nerdy Algos' +const project_name = 'TSP' + +const build_full_url = (uri: string): string => { + return Cypress.config().baseUrl + uri +} + +describe('project settings page as admin', () => { + beforeEach(() => { + cy.task('setup_db') + .create_course(course_name) + .as('course_pk', { type: 'static' }); + + cy.fake_login(username) + .get('@course_pk').then((course_pk) => { + cy.create_project(Number(course_pk), project_name).as('project_pk', { type: 'static' }); + }); + + cy.get('@project_pk').then(pk => { + cy.wrap(`/web/project_admin/${pk}`).as('page_uri') + }); + }) + + it('has functioning navbar tabs', function() { + cy.visit(this.page_uri) + .get_by_testid('settings-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=settings`)) + + cy.visit(this.page_uri) + .get_by_testid('instructor-files-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=instructor_files`)) + + cy.visit(this.page_uri) + .get_by_testid('instructor-files-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=instructor_files`)) + + cy.visit(this.page_uri) + .get_by_testid('student-files-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=expected_student_files`)) + + cy.visit(this.page_uri) + .get_by_testid('test-cases-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=test_cases`)) + + cy.visit(this.page_uri) + .get_by_testid('mutation-testing-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=mutation_testing`)) + + cy.visit(this.page_uri) + .get_by_testid('edit-groups-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=edit_groups`)) + + cy.visit(this.page_uri) + .get_by_testid('download-grades-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=download_grades`)) + + cy.visit(this.page_uri) + .get_by_testid('rerun-tests-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=rerun_tests`)) + + cy.visit(this.page_uri) + .get_by_testid('handgrading-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=handgrading`)) + + cy.visit(this.page_uri) + .get_by_testid('stats-tab') + .should('be.visible').click() + .url().should('eq', build_full_url(`${this.page_uri}?current_tab=stats`)) + }); + + it('allows user to set and delete soft deadline', function() { + // April 3, 2024 12:01 PM + const now = new Date(2024, 3, 14, 12, 1) + cy.clock(now) + + const new_date = '15' + const new_hours = '2' + const new_minutes = '30' + + // See FIXME below... + const new_datetime_str = 'April 15, 2024, 02:30 PM PDT' + + cy.visit(this.page_uri) + .get_by_testid('soft-deadline-input').should('be.visible').click(); + + cy.get_by_testid('soft-deadline-picker').should('be.visible').find('.available-day') + .contains(new_date).should('be.visible').click(); + + cy.get_by_testid('period-input').should('be.visible').should('have.value', 'PM').click(); + + cy.get_by_testid('hour-input').should('be.visible').should('have.value', '12') + .type(new_hours); + + cy.get_by_testid('minute-input').should('be.visible').should('have.value', '01') + .type(new_minutes); + + // TZ environment variable is set within the npm script + cy.get_by_testid('timezone-select').should('have.value', 'America/Los_Angeles'); + + // FIXME?: It feels weird that the period automatically switches to AM when typing a new value + cy.get_by_testid('period-input').should('be.visible').should('have.value', 'AM').click(); + + // check date has been updated on page + cy.get_by_testid('soft-deadline-input').should('contain.text', new_datetime_str) + + // save amd check that new data is loaded on refresh + cy.get_by_testid('save-button').should('be.visible').click() + + cy.reload().get_by_testid('soft-deadline-input').should('contain.text', new_datetime_str) + }) +}) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 698b01a42..56874212a 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -34,4 +34,15 @@ // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable // } // } -// } \ No newline at end of file +// } +// + +declare namespace Cypress { + interface Chainable { + get_by_testid(selector: string, ...args): Chainable + } +} + +Cypress.Commands.add('get_by_testid', (selector, ...args) => { + return cy.get(`[data-testid=${selector}]`, ...args) +}) diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index f80f74f8e..aba5141bd 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -13,8 +13,70 @@ // https://on.cypress.io/configuration // *********************************************************** -// Import commands.js using ES2015 syntax: import './commands' -// Alternatively you can use CommonJS syntax: -// require('./commands') \ No newline at end of file +const { superuser, admin, staff, student } = Cypress.env() + +declare global { + namespace Cypress { + interface Chainable { + create_course(course_name: string): Chainable + create_project(course_pk: number, project_name: string): Chainable + fake_login(username: string): Chainable + logout(): Chainable + } + } +} + +Cypress.Commands.add('fake_login', username => { + cy.setCookie('token', 'foo').setCookie('username', username) +}) + +Cypress.Commands.add('logout', () => { + cy.clearAllCookies() +}) + +/* + * Create a course by making a POST request to the API and yield the pk + */ +Cypress.Commands.add('create_course', course_name => { + cy.fake_login(superuser) + + cy.request('POST', '/api/courses/', { + 'name': course_name, + 'semester': 'Fall', + 'year': 2024, + 'subtitle': 'This is a subtitle', + 'num_late_days': 0, + 'allowed_guest_domain': 'autograder.io' + }) + .then((res) => cy.wrap(res.body.pk)) + .then((pk) => { + cy.request('POST', `/api/courses/${pk}/admins/`, { + 'new_admins': [ admin ] + }); + return cy.wrap(pk); + }) + .then((pk) => { + cy.request('POST', `/api/courses/${pk}/staff/`, { + 'new_staff': [ staff ] + }); + return cy.wrap(pk); + }) + .then((pk) => { + cy.request('POST', `/api/courses/${pk}/students/`, { + 'new_students': [ student ] + }); + cy.logout(); + return cy.wrap(pk); + }) +}) + +/* + * Create a project by making a POST request to the API and yield the pk + */ +Cypress.Commands.add('create_project', (course_pk, project_name) => { + cy.request('POST', `/api/courses/${course_pk}/projects/`, { + 'name': project_name + }).then((res) => cy.wrap(res.body.pk)) +}) diff --git a/package.json b/package.json index f09dd667e..50a4bf3ac 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,17 @@ "version": "2024.08.0", "private": true, "scripts": { - "serve:e2e": "docker compose -f cypress/e2e/test_stack/docker-compose.yml up", - "migrate:e2e": "docker exec -it ag-vue-e2e-django python3 manage.py migrate", - "cy:e2e": "npm run migrate:e2e && cypress run", - "e2e": "start-server-and-test serve:e2e http://localhost:8080 cy:e2e", + "e2e:serve": "docker compose -f cypress/e2e/test_stack/docker-compose.yml up $([ ${CI} = 'true' ] && echo '-d')", + "e2e:migrate": "docker exec -it ag-vue-e2e-django python3 manage.py migrate", + "e2e:cy:run": "npm run e2e:migrate && TZ=America/Los_Angeles cypress run", + "e2e:cy:open": "npm run e2e:migrate && TZ=America/Los_Angeles cypress open", + "e2e": "start-server-and-test e2e:serve http://localhost:8080 e2e:cy:run", + "e2e:browser": "start-server-and-test e2e:serve http://localhost:8080 e2e:cy:open", "serve": "vue-cli-service serve", "lint": "./static_analysis.bash", "build": "vue-cli-service build", - "test": "python3 run_tests.py && cypress run --component" + "component": "python3 run_tests.py && cypress run --component", + "test": "npm run component && npm run e2e" }, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", diff --git a/src/components/datetime/time_picker.vue b/src/components/datetime/time_picker.vue index 4bfd51f0c..179dd1130 100644 --- a/src/components/datetime/time_picker.vue +++ b/src/components/datetime/time_picker.vue @@ -10,7 +10,7 @@
@@ -34,6 +34,7 @@
@@ -51,6 +52,7 @@
diff --git a/src/components/project_admin/project_admin.vue b/src/components/project_admin/project_admin.vue index b9515e4c1..13fc80ee7 100644 --- a/src/components/project_admin/project_admin.vue +++ b/src/components/project_admin/project_admin.vue @@ -5,53 +5,64 @@
+

HI!