From 9a589dc6cbeca6976373a5ba89dc952a2fcdfdc8 Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 11 Oct 2018 21:37:37 -0400 Subject: [PATCH 01/68] Remove unneeded file headers from test files --- test/components/backup/new.spec.js | 11 ----------- test/components/backup/terminate.spec.js | 11 ----------- test/components/field-key/new.spec.js | 11 ----------- test/components/field-key/revoke.spec.js | 11 ----------- test/components/user/new.spec.js | 11 ----------- test/components/user/reset-password.spec.js | 11 ----------- 6 files changed, 66 deletions(-) diff --git a/test/components/backup/new.spec.js b/test/components/backup/new.spec.js index 7a873de20..b6e1e41da 100644 --- a/test/components/backup/new.spec.js +++ b/test/components/backup/new.spec.js @@ -1,14 +1,3 @@ -/* -Copyright 2017 ODK Central Developers -See the NOTICE file at the top-level directory of this distribution and at -https://github.com/opendatakit/central-frontend/blob/master/NOTICE. - -This file is part of ODK Central. It is subject to the license terms in -the LICENSE file found in the top-level directory of this distribution and at -https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, -including this file, may be copied, modified, propagated, or distributed -except according to the terms contained in the LICENSE file. -*/ import App from '../../../lib/components/app.vue'; import BackupList from '../../../lib/components/backup/list.vue'; import BackupNew from '../../../lib/components/backup/new.vue'; diff --git a/test/components/backup/terminate.spec.js b/test/components/backup/terminate.spec.js index 9adf3b246..914a13d86 100644 --- a/test/components/backup/terminate.spec.js +++ b/test/components/backup/terminate.spec.js @@ -1,14 +1,3 @@ -/* -Copyright 2017 ODK Central Developers -See the NOTICE file at the top-level directory of this distribution and at -https://github.com/opendatakit/central-frontend/blob/master/NOTICE. - -This file is part of ODK Central. It is subject to the license terms in -the LICENSE file found in the top-level directory of this distribution and at -https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, -including this file, may be copied, modified, propagated, or distributed -except according to the terms contained in the LICENSE file. -*/ import BackupList from '../../../lib/components/backup/list.vue'; import BackupTerminate from '../../../lib/components/backup/terminate.vue'; import testData from '../../data'; diff --git a/test/components/field-key/new.spec.js b/test/components/field-key/new.spec.js index 494ae23b3..732d27e83 100644 --- a/test/components/field-key/new.spec.js +++ b/test/components/field-key/new.spec.js @@ -1,14 +1,3 @@ -/* -Copyright 2017 ODK Central Developers -See the NOTICE file at the top-level directory of this distribution and at -https://github.com/opendatakit/central-frontend/blob/master/NOTICE. - -This file is part of ODK Central. It is subject to the license terms in -the LICENSE file found in the top-level directory of this distribution and at -https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, -including this file, may be copied, modified, propagated, or distributed -except according to the terms contained in the LICENSE file. -*/ import FieldKeyList from '../../../lib/components/field-key/list.vue'; import FieldKeyNew from '../../../lib/components/field-key/new.vue'; import testData from '../../data'; diff --git a/test/components/field-key/revoke.spec.js b/test/components/field-key/revoke.spec.js index 3790e596d..d1d5aaf31 100644 --- a/test/components/field-key/revoke.spec.js +++ b/test/components/field-key/revoke.spec.js @@ -1,14 +1,3 @@ -/* -Copyright 2017 ODK Central Developers -See the NOTICE file at the top-level directory of this distribution and at -https://github.com/opendatakit/central-frontend/blob/master/NOTICE. - -This file is part of ODK Central. It is subject to the license terms in -the LICENSE file found in the top-level directory of this distribution and at -https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, -including this file, may be copied, modified, propagated, or distributed -except according to the terms contained in the LICENSE file. -*/ import FieldKeyList from '../../../lib/components/field-key/list.vue'; import FieldKeyRevoke from '../../../lib/components/field-key/revoke.vue'; import testData from '../../data'; diff --git a/test/components/user/new.spec.js b/test/components/user/new.spec.js index e5df6bfeb..184b59f25 100644 --- a/test/components/user/new.spec.js +++ b/test/components/user/new.spec.js @@ -1,14 +1,3 @@ -/* -Copyright 2017 ODK Central Developers -See the NOTICE file at the top-level directory of this distribution and at -https://github.com/opendatakit/central-frontend/blob/master/NOTICE. - -This file is part of ODK Central. It is subject to the license terms in -the LICENSE file found in the top-level directory of this distribution and at -https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, -including this file, may be copied, modified, propagated, or distributed -except according to the terms contained in the LICENSE file. -*/ import UserList from '../../../lib/components/user/list.vue'; import UserNew from '../../../lib/components/user/new.vue'; import testData from '../../data'; diff --git a/test/components/user/reset-password.spec.js b/test/components/user/reset-password.spec.js index 1b68e5ed0..7d63cbd7c 100644 --- a/test/components/user/reset-password.spec.js +++ b/test/components/user/reset-password.spec.js @@ -1,14 +1,3 @@ -/* -Copyright 2017 ODK Central Developers -See the NOTICE file at the top-level directory of this distribution and at -https://github.com/opendatakit/central-frontend/blob/master/NOTICE. - -This file is part of ODK Central. It is subject to the license terms in -the LICENSE file found in the top-level directory of this distribution and at -https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, -including this file, may be copied, modified, propagated, or distributed -except according to the terms contained in the LICENSE file. -*/ import UserList from '../../../lib/components/user/list.vue'; import UserResetPassword from '../../../lib/components/user/reset-password.vue'; import testData from '../../data'; From 286a5b51484af90c8a646130f96a8eed7a136744 Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 11 Oct 2018 21:37:39 -0400 Subject: [PATCH 02/68] Rename faker.app to faker.central Making this change in order to clarify that these functions are Central-specific and not about apps in general. --- test/components/backup/new.spec.js | 2 +- test/data/fieldKeys.js | 2 +- test/data/sessions.js | 2 +- test/faker.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/components/backup/new.spec.js b/test/components/backup/new.spec.js index b6e1e41da..20944511d 100644 --- a/test/components/backup/new.spec.js +++ b/test/components/backup/new.spec.js @@ -33,7 +33,7 @@ const moveToStep3 = (component) => moveToStep1(component) .request(next1) .respondWithData(() => ({ url: 'http://localhost', - token: faker.app.token() + token: faker.central.token() })) .afterResponse(next2); // For step 3, fills the form and clicks the Next button. diff --git a/test/data/fieldKeys.js b/test/data/fieldKeys.js index b2f53685b..b49190e65 100644 --- a/test/data/fieldKeys.js +++ b/test/data/fieldKeys.js @@ -14,7 +14,7 @@ export const extendedFieldKeys = dataStore({ return { id, displayName: faker.name.findName(), - token: faker.random.arrayElement([faker.app.token(), null]), + token: faker.random.arrayElement([faker.central.token(), null]), meta: null, lastUsed: inPast && faker.random.boolean() ? faker.date.pastSince(createdAt).toISOString() diff --git a/test/data/sessions.js b/test/data/sessions.js index 5eb4b3df7..7ae490908 100644 --- a/test/data/sessions.js +++ b/test/data/sessions.js @@ -4,7 +4,7 @@ import { dataStore } from './data-store'; // eslint-disable-next-line import/prefer-default-export export const sessions = dataStore({ factory: ({ inPast, lastCreatedAt }) => ({ - token: faker.app.token(), + token: faker.central.token(), expiresAt: faker.date.future().toISOString(), createdAt: faker.date.timestamps(inPast, [lastCreatedAt]).createdAt }) diff --git a/test/faker.js b/test/faker.js index 2ea88d90f..fbd991950 100644 --- a/test/faker.js +++ b/test/faker.js @@ -62,7 +62,7 @@ Object.assign(fakerExtensions, { return { createdAt, updatedAt }; } }, - app: { + central: { token: () => faker.random.alphaNumeric(64) } }); From e1922154bbda6c860163a19f055817b90aa74d3f Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 11 Oct 2018 21:37:41 -0400 Subject: [PATCH 03/68] Replace testData validations with faker functions Tests are also now generally just trusted not to pass invalid options to factories. I am making this change for three reasons: 1. It reduces the complexity of testData, which I think will make it easier for contributors to onboard to Frontend. 2. As-is, validations are hardly used. 3. The one validation that is in use, validateUniqueCombination(), does not support updating a test data object, which we need to do as part of the AccountEdit tests. --- test/data/administrators.js | 6 +----- test/data/data-store.js | 9 +-------- test/data/forms.js | 7 +------ test/data/submissions.js | 4 ---- test/data/validate.js | 16 ---------------- test/faker.js | 33 ++++++++++++++++++++++++++++++++- test/setup.js | 2 ++ 7 files changed, 37 insertions(+), 40 deletions(-) delete mode 100644 test/data/validate.js diff --git a/test/data/administrators.js b/test/data/administrators.js index d8505e2a2..fd9f32c1b 100644 --- a/test/data/administrators.js +++ b/test/data/administrators.js @@ -1,18 +1,14 @@ import faker from '../faker'; import { dataStore } from './data-store'; -import { validateUniqueCombination } from './validate'; // eslint-disable-next-line import/prefer-default-export export const administrators = dataStore({ factory: ({ inPast, id, lastCreatedAt }) => ({ id, displayName: faker.name.findName(), - email: faker.internet.email(), + email: faker.internet.uniqueEmail(), meta: null, ...faker.date.timestamps(inPast, [lastCreatedAt]) }), - validate: [ - validateUniqueCombination(['email']) - ], sort: 'email' }); diff --git a/test/data/data-store.js b/test/data/data-store.js index f959498e9..541e26c45 100644 --- a/test/data/data-store.js +++ b/test/data/data-store.js @@ -5,7 +5,6 @@ class Factory { constructor(store, options) { this._store = store; this._options = { ...options }; - if (this._options.validate == null) this._options.validate = []; this.reset(); } @@ -92,10 +91,7 @@ class Factory { } _isValid(object, constraints) { - const validators = constraints.length !== 0 - ? [...this._options.validate, ...constraints] - : this._options.validate; - for (const validator of validators) + for (const validator of constraints) if (!validator(object, this._store)) return false; return true; } @@ -197,9 +193,6 @@ class Store extends Collection { // updates to createdAt. throw new Error('createdAt cannot be updated'); } - for (const validator of this._factory.options().validate) - if (!validator(object, this)) - throw new Error('object is no longer valid'); } clear() { diff --git a/test/data/forms.js b/test/data/forms.js index c90e398f3..91783e0e1 100644 --- a/test/data/forms.js +++ b/test/data/forms.js @@ -4,7 +4,6 @@ import faker from '../faker'; import { administrators } from './administrators'; import { dataStore, view } from './data-store'; import { sortByUpdatedAtOrCreatedAtDesc } from './sort'; -import { validateUniqueCombination } from './validate'; export const extendedForms = dataStore({ factory: ({ @@ -16,7 +15,6 @@ export const extendedForms = dataStore({ isOpen = !inPast || faker.random.boolean(), hasSubmission = inPast && faker.random.boolean() }) => { - const xmlFormId = `a${faker.random.alphaNumeric(8)}`; const name = hasName ? faker.name.findName() : null; const version = faker.random.boolean() ? faker.random.number().toString() : ''; const createdBy = administrators.randomOrCreatePast(); @@ -32,7 +30,7 @@ export const extendedForms = dataStore({ }); } return { - xmlFormId, + xmlFormId: faker.central.xmlFormId(), name, version, state: isOpen ? 'open' : faker.random.arrayElement(['closing', 'closed']), @@ -78,9 +76,6 @@ export const extendedForms = dataStore({ ] }; }, - validate: [ - validateUniqueCombination(['xmlFormId']) - ], sort: sortByUpdatedAtOrCreatedAtDesc }); diff --git a/test/data/submissions.js b/test/data/submissions.js index 5fcd21575..48ea874c7 100644 --- a/test/data/submissions.js +++ b/test/data/submissions.js @@ -6,7 +6,6 @@ import { administrators } from './administrators'; import { dataStore } from './data-store'; import { extendedForms } from './forms'; import { sortByUpdatedAtOrCreatedAtDesc } from './sort'; -import { validateUniqueCombination } from './validate'; // eslint-disable-next-line import/prefer-default-export export const extendedSubmissions = dataStore({ @@ -116,9 +115,6 @@ export const extendedSubmissions = dataStore({ updatedAt }; }, - validate: [ - validateUniqueCombination(['formId', 'instanceId']) - ], sort: sortByUpdatedAtOrCreatedAtDesc }); diff --git a/test/data/validate.js b/test/data/validate.js deleted file mode 100644 index 740696a6e..000000000 --- a/test/data/validate.js +++ /dev/null @@ -1,16 +0,0 @@ -import R from 'ramda'; - -export const validateDateOrder = (path1, path2) => (object) => { - const date1 = R.path(path1.split('.'), object); - if (date1 == null) return true; - const date2 = R.path(path2.split('.'), object); - return date2 == null || date1 <= date2; -}; - -export const validateUniqueCombination = (propertyNames) => (object, store) => { - for (let i = 0; i < store.size; i += 1) { - if (propertyNames.every(name => object[name] === store.get(i)[name])) - return false; - } - return true; -}; diff --git a/test/faker.js b/test/faker.js index fbd991950..f3233e320 100644 --- a/test/faker.js +++ b/test/faker.js @@ -27,6 +27,33 @@ const faker = new Proxy({}, { +//////////////////////////////////////////////////////////////////////////////// +// UNIQUE RESULTS + +const uniqueResults = []; + +// Returns a function that invokes `callback` until it returns a value that has +// not been seen before. +const uniqueResult = (callback) => { + const results = new Set(); + uniqueResults.push(results); + return (...args) => { + let result; + do { + result = callback(...args); + } while (results.has(result)); + results.add(result); + return result; + }; +}; + +export const clearUniqueFakerResults = () => { + for (const results of uniqueResults) + results.clear(); +}; + + + //////////////////////////////////////////////////////////////////////////////// // EXTENSIONS @@ -62,8 +89,12 @@ Object.assign(fakerExtensions, { return { createdAt, updatedAt }; } }, + internet: { + uniqueEmail: uniqueResult(() => faker.internet.email()) + }, central: { - token: () => faker.random.alphaNumeric(64) + token: uniqueResult(() => faker.random.alphaNumeric(64)), + xmlFormId: uniqueResult(() => `a${faker.random.alphaNumeric(8)}`) } }); diff --git a/test/setup.js b/test/setup.js index a059f904c..ac7919e54 100644 --- a/test/setup.js +++ b/test/setup.js @@ -11,6 +11,7 @@ import { ComponentAlert, closestComponentWithAlert } from '../lib/alert'; import { MockComponentAlert } from './alert'; import { MockLogger } from './util'; import { clearNavGuards, initNavGuards } from './router'; +import { clearUniqueFakerResults } from './faker'; import { destroyMarkedComponent, mountAndMark } from './destroy'; import { logOut } from '../lib/session'; import { router } from '../lib/router'; @@ -62,3 +63,4 @@ afterEach(() => { }); afterEach(testData.reset); +afterEach(clearUniqueFakerResults); From 148ef88644f794f261737486dce711fe6bcac0d4 Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 11 Oct 2018 21:37:42 -0400 Subject: [PATCH 04/68] Add submitForm() test helper --- test/components/account/claim.spec.js | 24 ++++++++++--------- .../components/account/reset-password.spec.js | 21 ++++++++-------- test/components/backup/new.spec.js | 13 ++++------ test/components/field-key/new.spec.js | 20 +++++++--------- test/components/user/new.spec.js | 14 ++++++----- test/event.js | 17 +++++++++---- test/session.js | 13 ++++------ test/util.js | 4 ++-- 8 files changed, 64 insertions(+), 62 deletions(-) diff --git a/test/components/account/claim.spec.js b/test/components/account/claim.spec.js index 2291d5fa0..29ea52ba5 100644 --- a/test/components/account/claim.spec.js +++ b/test/components/account/claim.spec.js @@ -1,14 +1,10 @@ import Navbar from '../../../lib/components/navbar.vue'; import testData from '../../data'; -import { fillForm, trigger } from '../../util'; import { mockRoute } from '../../http'; import { mockRouteThroughLogin } from '../../session'; +import { submitForm } from '../../event'; const LOCATION = { path: '/account/claim', query: { token: 'a'.repeat(64) } }; -const submitForm = (wrapper) => - fillForm(wrapper, [['#account-claim input[type="password"]', 'password']]) - .then(() => trigger.submit(wrapper.first('#account-claim form'))) - .then(() => wrapper); describe('AccountClaim', () => { it('field is focused', () => @@ -20,7 +16,9 @@ describe('AccountClaim', () => { // from the URL. mockRoute(LOCATION) .complete() - .request(submitForm) + .request(app => submitForm(app, '#account-claim form', [ + ['input[type="password"]', 'password'] + ])) .standardButton()); it('navbar is visible', () => @@ -33,17 +31,21 @@ describe('AccountClaim', () => { let app; beforeEach(() => mockRoute(LOCATION) .complete() - .request(submitForm) - .respondWithSuccess() - .afterResponse(component => { + .request(component => { app = component; - })); + return submitForm(app, '#account-claim form', [ + ['input[type="password"]', 'password'] + ]); + }) + .respondWithSuccess()); it('user is redirected to login', () => { app.vm.$route.path.should.equal('/login'); }); - it('success message is shown', () => app.should.alert('success')); + it('success message is shown', () => { + app.should.alert('success'); + }); }); describe('navigation to /account/claim', () => { diff --git a/test/components/account/reset-password.spec.js b/test/components/account/reset-password.spec.js index 9fdf8f73a..bd8de45df 100644 --- a/test/components/account/reset-password.spec.js +++ b/test/components/account/reset-password.spec.js @@ -1,14 +1,7 @@ import testData from '../../data'; -import { fillForm, trigger } from '../../util'; import { mockRoute } from '../../http'; import { mockRouteThroughLogin } from '../../session'; - -const submitForm = (wrapper) => { - const { email } = testData.administrators.createPast(1).last(); - return fillForm(wrapper, [['#account-reset-password input[type="email"]', email]]) - .then(() => trigger.submit(wrapper.first('#account-reset-password form'))) - .then(() => wrapper); -}; +import { submitForm, trigger } from '../../event'; describe('AccountResetPassword', () => { it('field is focused', () => @@ -25,7 +18,9 @@ describe('AccountResetPassword', () => { mockRoute('/reset-password') .restoreSession(false) .complete() - .request(submitForm) + .request(app => submitForm(app, '#account-reset-password form', [ + ['input[type="email"]', testData.administrators.createPast(1).last().email] + ])) .standardButton()); describe('successful response', () => { @@ -35,7 +30,9 @@ describe('AccountResetPassword', () => { .complete() .request(component => { app = component; - submitForm(app); + return submitForm(app, '#account-reset-password form', [ + ['input[type="email"]', testData.administrators.createPast(1).last().email] + ]); }) .respondWithSuccess()); @@ -43,7 +40,9 @@ describe('AccountResetPassword', () => { app.vm.$route.path.should.equal('/login'); }); - it('shows a success message', () => app.should.alert('success')); + it('shows a success message', () => { + app.should.alert('success'); + }); }); it('clicking cancel navigates to login', () => diff --git a/test/components/backup/new.spec.js b/test/components/backup/new.spec.js index 20944511d..e8d7e17e6 100644 --- a/test/components/backup/new.spec.js +++ b/test/components/backup/new.spec.js @@ -3,9 +3,9 @@ import BackupList from '../../../lib/components/backup/list.vue'; import BackupNew from '../../../lib/components/backup/new.vue'; import faker from '../../faker'; import testData from '../../data'; -import { fillForm, trigger } from '../../util'; import { mockHttp, mockRoute } from '../../http'; import { mockLogin } from '../../session'; +import { submitForm, trigger } from '../../event'; const clickCreateButton = (wrapper) => trigger.click(wrapper.first('#backup-list-new-button')).then(() => wrapper); @@ -22,9 +22,7 @@ const moveToStep1 = (component) => { }; // For step 1, fills the form and clicks the Next button. const next1 = (wrapper) => - fillForm(wrapper, [['#backup-new input', '']]) - .then(() => trigger.submit(wrapper.first('#backup-new form'))) - .then(() => wrapper); + submitForm(wrapper, '#backup-new form', [['input', '']]); // For step 2, clicks the Next button. const next2 = (wrapper) => trigger.click(wrapper.first('#backup-new .btn-primary')) @@ -37,10 +35,9 @@ const moveToStep3 = (component) => moveToStep1(component) })) .afterResponse(next2); // For step 3, fills the form and clicks the Next button. -const next3 = (wrapper) => - fillForm(wrapper, [['#backup-new input', faker.random.alphaNumeric(57)]]) - .then(() => trigger.submit(wrapper.first('#backup-new form'))) - .then(() => wrapper); +const next3 = (wrapper) => submitForm(wrapper, '#backup-new form', [ + ['input', faker.random.alphaNumeric(57)] +]); const completeSetup = (component) => { if (component !== App && component !== BackupList) throw new Error('invalid component'); diff --git a/test/components/field-key/new.spec.js b/test/components/field-key/new.spec.js index 732d27e83..fbb42076b 100644 --- a/test/components/field-key/new.spec.js +++ b/test/components/field-key/new.spec.js @@ -1,19 +1,12 @@ import FieldKeyList from '../../../lib/components/field-key/list.vue'; import FieldKeyNew from '../../../lib/components/field-key/new.vue'; import testData from '../../data'; -import { fillForm, trigger } from '../../util'; import { mockHttp, mockRoute } from '../../http'; import { mockLogin } from '../../session'; +import { submitForm, trigger } from '../../event'; const clickCreateButton = (wrapper) => - trigger.click(wrapper.first('#field-key-list-new-button')) - .then(() => wrapper); -const submitForm = (wrapper) => { - const nickname = testData.extendedFieldKeys.createNew('withAccess').displayName; - return fillForm(wrapper, [['#field-key-new input', nickname]]) - .then(() => trigger.submit(wrapper.first('#field-key-new form'))) - .then(() => wrapper); -}; + trigger.click(wrapper, '#field-key-list-new-button'); describe('FieldKeyNew', () => { beforeEach(mockLogin); @@ -48,7 +41,9 @@ describe('FieldKeyNew', () => { it('standard button thinking things', () => mockHttp() .mount(FieldKeyNew) - .request(submitForm) + .request(modal => submitForm(modal, 'form', [ + ['input', testData.extendedFieldKeys.createNew('withAccess').displayName] + ])) .standardButton()); describe('after successful submit', () => { @@ -58,7 +53,10 @@ describe('FieldKeyNew', () => { .afterResponse(component => { app = component; }) - .request(() => clickCreateButton(app).then(submitForm)) + .request(() => clickCreateButton(app) + .then(() => submitForm(app, '#field-key-new form', [ + ['input', testData.extendedFieldKeys.createNew('withAccess').displayName] + ]))) .respondWithData(() => testData.simpleFieldKeys.last())); const testCreationCompletion = () => { diff --git a/test/components/user/new.spec.js b/test/components/user/new.spec.js index 184b59f25..5f6e97b11 100644 --- a/test/components/user/new.spec.js +++ b/test/components/user/new.spec.js @@ -1,15 +1,12 @@ import UserList from '../../../lib/components/user/list.vue'; import UserNew from '../../../lib/components/user/new.vue'; import testData from '../../data'; -import { fillForm, trigger } from '../../util'; import { mockHttp, mockRoute } from '../../http'; import { mockLogin } from '../../session'; +import { submitForm, trigger } from '../../event'; const clickCreateButton = (wrapper) => trigger.click(wrapper, '#user-list-new-button'); -const submitForm = (wrapper) => - fillForm(wrapper, [['[type="email"]', testData.administrators.createNew().email]]) - .then(() => trigger.submit(wrapper, '#user-new form')); describe('UserNew', () => { beforeEach(mockLogin); @@ -42,7 +39,9 @@ describe('UserNew', () => { it('standard button thinking things', () => mockHttp() .mount(UserNew) - .request(submitForm) + .request(modal => submitForm(modal, 'form', [ + ['input[type="email"]', testData.administrators.createNew().email] + ])) .standardButton()); describe('after successful submit', () => { @@ -52,7 +51,10 @@ describe('UserNew', () => { .afterResponse(component => { app = component; }) - .request(() => clickCreateButton(app).then(submitForm)) + .request(() => clickCreateButton(app) + .then(() => submitForm(app, '#user-new form', [ + ['input[type="email"]', testData.administrators.createNew().email] + ]))) .respondWithData(() => testData.administrators.last()) .respondWithData(() => testData.administrators.sorted())); diff --git a/test/event.js b/test/event.js index 00b2d07ce..45ede75bd 100644 --- a/test/event.js +++ b/test/event.js @@ -88,15 +88,22 @@ trigger.dragAndDrop = (...args) => trigger.dragenter(...args) // TRIGGERING A SERIES OF EVENTS export const fillForm = (wrapper, selectorsAndValues) => { - let promise = Promise.resolve(); - for (const [selector, value] of selectorsAndValues) { - promise = promise.then(() => { + const promise = selectorsAndValues.reduce( + (acc, [selector, value]) => acc.then(() => { const field = wrapper.first(selector); field.element.value = value; // If there is a v-model attribute, prompt it to sync. field.trigger('input'); return Vue.nextTick(); - }); - } + }), + Promise.resolve() + ); return promise; }; + +export const submitForm = (wrapper, formSelector, fieldSelectorsAndValues) => { + const form = wrapper.first(formSelector); + return fillForm(form, fieldSelectorsAndValues) + .then(() => trigger.submit(form)) + .then(() => wrapper); +}; diff --git a/test/session.js b/test/session.js index fe5a20720..2182ba2d3 100644 --- a/test/session.js +++ b/test/session.js @@ -1,9 +1,9 @@ import Vue from 'vue'; import testData from './data'; -import { fillForm, trigger } from './util'; import { logIn } from '../lib/session'; import { mockRoute } from './http'; +import { submitForm } from './event'; export const mockLogin = () => { if (testData.administrators.size !== 0) @@ -11,14 +11,11 @@ export const mockLogin = () => { logIn(testData.sessions.createNew(), testData.administrators.createPast(1).first()); }; -export const submitLoginForm = (wrapper) => { - const { email } = testData.administrators.firstOrCreatePast(); - const promise = fillForm(wrapper, [ - ['#account-login input[type="email"]', email], - ['#account-login input[type="password"]', 'password'] +export const submitLoginForm = (wrapper) => + submitForm(wrapper, '#account-login form', [ + ['input[type="email"]', testData.administrators.firstOrCreatePast().email], + ['input[type="password"]', 'password'] ]); - return promise.then(() => trigger.submit(wrapper, '#account-login form')); -}; export const mockRouteThroughLogin = (location, mountOptions = {}) => { if (Vue.prototype.$session.loggedIn()) diff --git a/test/util.js b/test/util.js index 3fd18abe1..12b5f0c4f 100644 --- a/test/util.js +++ b/test/util.js @@ -1,5 +1,5 @@ -// Deprecated. Access these directly from './event' instead. -export { trigger, fillForm } from './event'; +// Deprecated. Import `trigger` directly from './event' instead. +export { trigger } from './event'; export const MAXIMUM_TEST_DURATION = { seconds: 10 }; From 7a6aef10be12cd1120f0340ae155f87b151c02f3 Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 11 Oct 2018 21:37:44 -0400 Subject: [PATCH 05/68] Have fillForm() return the avoriaz wrapper, as trigger() does --- test/event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/event.js b/test/event.js index 45ede75bd..132d376f0 100644 --- a/test/event.js +++ b/test/event.js @@ -98,7 +98,7 @@ export const fillForm = (wrapper, selectorsAndValues) => { }), Promise.resolve() ); - return promise; + return promise.then(() => wrapper); }; export const submitForm = (wrapper, formSelector, fieldSelectorsAndValues) => { From 4b3472963ff8b1b7e524d765ec1cbf5512d78f4a Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 11 Oct 2018 21:37:45 -0400 Subject: [PATCH 06/68] Update search for spinner in tests of standard button thinking things --- test/http.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/http.js b/test/http.js index e8309e46e..9dde11454 100644 --- a/test/http.js +++ b/test/http.js @@ -568,7 +568,11 @@ class MockHttp { // Tests standard button thinking things. standardButton(buttonSelector = 'button[type="submit"]') { const spinner = (button) => { - const spinners = button.find(Spinner); + const spinners = button + .find(Spinner) + // I think find() in the previous line starts the search from the + // button's parent Vue component. + .filter(wrapper => $.contains(button.element, wrapper.vm.$el)); if (spinners.length === 0) throw new Error('spinner not found'); if (spinners.length > 1) throw new Error('multiple spinners found'); return spinners[0]; From 8847322b39ce008df5e9a6a0c38130b1525c4b42 Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 11 Oct 2018 21:37:47 -0400 Subject: [PATCH 07/68] Add support for PUT requests --- lib/mixins/request.js | 5 +++++ test/http.js | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/mixins/request.js b/lib/mixins/request.js index 8bff363b8..b34a08a12 100644 --- a/lib/mixins/request.js +++ b/lib/mixins/request.js @@ -100,6 +100,10 @@ function post(url, data, config) { return this.request({ ...config, method: 'post', url, data }); } +function put(url, data, config) { + return this.request({ ...config, method: 'put', url, data }); +} + function patch(url, data, config) { return this.request({ ...config, method: 'patch', url, data }); } @@ -117,6 +121,7 @@ export default () => { // eslint-disable-line arrow-body-style requestAll, get, post, + put, patch, delete: del } diff --git a/test/http.js b/test/http.js index 9dde11454..0968c87ec 100644 --- a/test/http.js +++ b/test/http.js @@ -19,6 +19,7 @@ export const setHttp = (respond) => { http.request = http; http.get = (url, config) => http({ ...config, method: 'get', url }); http.post = (url, data, config) => http({ ...config, method: 'post', url, data }); + http.put = (url, data, config) => http({ ...config, method: 'put', url, data }); http.patch = (url, data, config) => http({ ...config, method: 'patch', url, data }); http.delete = (url, config) => http({ ...config, method: 'delete', url }); http.defaults = { From ee2636482d3f8e6f47f4fefecbcc5586a153bdff Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 11 Oct 2018 21:37:48 -0400 Subject: [PATCH 08/68] Have App pass session to Navbar This moves router-related logic from Navbar to App, centralizing it there. --- lib/components/app.vue | 42 +++++++++++++++++++++++++++++++-------- lib/components/navbar.vue | 27 +++++++++---------------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/lib/components/app.vue b/lib/components/app.vue index 093eae1e1..3ed66aefb 100644 --- a/lib/components/app.vue +++ b/lib/components/app.vue @@ -14,10 +14,10 @@ except according to the terms contained in the LICENSE file. - +
- +
@@ -35,13 +35,27 @@ export default { /* Vue seems to trigger the initial navigation before creating App. If the initial navigation is synchronous, Vue seems to confirm the navigation before creating App -- in which case firstNavigationConfirmed will be - initialized to true and the $route() watcher will not be called until the + initialized to true and the $route watcher will not be called until the user navigates elsewhere. However, if the initial navigation is asynchronous, Vue seems to create App before waiting to confirm the navigation. In that case, firstNavigationConfirmed will be initialized to - false and the $route() watcher will be called once the initial navigation - is confirmed. */ + false and the $route watcher will be called once the initial navigation is + confirmed. */ firstNavigationConfirmed: routerState.navigations.first.confirmed, + /* + this.$session is not a reactive property, so we store a copy of it here in + order to pass it to Navbar. This copy can change in one of two ways: + + 1. The router changes $session along with $route. App watches for + changes to $route, which is a reactive property. + 2. The router view changes $session, then notes the change by triggering + an update:session event. + + Between the router, session, and alert, App is doing a fair amount of + global state management at this point. We may end up wanting to implement + a more comprehensive state management strategy. + */ + session: this.$session, alert: blankAlert() }; }, @@ -51,12 +65,19 @@ export default { } }, watch: { + $route() { + this.firstNavigationConfirmed = true; + this.session = this.$session; + }, // Using a strategy similar to the one here: // https://github.com/vuejs/vue/issues/844 routeAndAlert([currentRoute, currentAlert], [previousRoute, previousAlert]) { - if (currentRoute === previousRoute) return; - this.firstNavigationConfirmed = true; - if (currentAlert === previousAlert && this.alert.state) + // If both the route and alert have changed, the router view will be + // updated, and if the new alert is visible, it will be shown. On the + // other hand, if only the route has changed, then if there is an alert + // currently visible, it will be hidden. + if (currentRoute !== previousRoute && currentAlert === previousAlert && + this.alert.state) this.alert.state = false; } }, @@ -66,6 +87,11 @@ export default { $(this.$refs.app).on('click', 'a.disabled', (event) => { event.preventDefault(); }); + }, + methods: { + updateSession() { + this.session = this.$session; + } } }; diff --git a/lib/components/navbar.vue b/lib/components/navbar.vue index f3933c88b..9572c5fe9 100644 --- a/lib/components/navbar.vue +++ b/lib/components/navbar.vue @@ -37,7 +37,7 @@ except according to the terms contained in the LICENSE file.