diff --git a/shared-libs/search/src/generate-search-requests.js b/shared-libs/search/src/generate-search-requests.js index dcb936001e3..11c4fd6abed 100644 --- a/shared-libs/search/src/generate-search-requests.js +++ b/shared-libs/search/src/generate-search-requests.js @@ -200,9 +200,9 @@ const makeCombinedParams = (freetextRequest, typeKey) => { return params; }; -const getContactsByTypeAndFreetextRequest = (typeRequests, freetextRequest) => { +const getContactsByTypeAndFreetextRequest = (typeRequests, freetextRequest, freetextDdocName) => { const result = { - view: 'medic-client/contacts_by_type_freetext', + view: `${freetextDdocName}/contacts_by_type_freetext`, union: typeRequests.params.keys.length > 1 }; @@ -217,9 +217,9 @@ const getContactsByTypeAndFreetextRequest = (typeRequests, freetextRequest) => { return result; }; -const getCombinedContactsRequests = (freetextRequests, contactsByParentRequest, typeRequest) => { +const getCombinedContactsRequests = (freetextRequests, contactsByParentRequest, typeRequest, freetextDdocName) => { const combinedRequests = freetextRequests.map(freetextRequest => { - return getContactsByTypeAndFreetextRequest(typeRequest, freetextRequest); + return getContactsByTypeAndFreetextRequest(typeRequest, freetextRequest, freetextDdocName); }); if (contactsByParentRequest) { combinedRequests.unshift(contactsByParentRequest); @@ -241,14 +241,14 @@ const setDefaultContactsRequests = (requests, shouldSortByLastVisitedDate) => { }; const requestBuilders = { - reports: (filters) => { + reports: (filters, freetextDdocName) => { let requests = [ reportedDateRequest(filters), formRequest(filters), validityRequest(filters), verificationRequest(filters), placeRequest(filters), - freetextRequest(filters, 'medic-client/reports_by_freetext'), + freetextRequest(filters, `${freetextDdocName}/reports_by_freetext`), subjectRequest(filters) ]; @@ -258,10 +258,10 @@ const requestBuilders = { } return requests; }, - contacts: (filters, extensions) => { + contacts: (filters, freetextDdocName, extensions) => { const shouldSortByLastVisitedDate = module.exports.shouldSortByLastVisitedDate(extensions); - const freetextRequests = freetextRequest(filters, 'medic-client/contacts_by_freetext'); + const freetextRequests = freetextRequest(filters, `${freetextDdocName}/contacts_by_freetext`); const contactsByParentRequest = getContactsByParentRequest(filters); const typeRequest = contactTypeRequest(filters, shouldSortByLastVisitedDate); const hasTypeRequest = typeRequest?.params.keys.length; @@ -272,7 +272,7 @@ const requestBuilders = { } if (hasTypeRequest && freetextRequests?.length) { - return getCombinedContactsRequests(freetextRequests, contactsByParentRequest, typeRequest); + return getCombinedContactsRequests(freetextRequests, contactsByParentRequest, typeRequest, freetextDdocName); } const requests = _.compact(_.flatten([ freetextRequests, typeRequest, contactsByParentRequest ])); @@ -313,12 +313,13 @@ const requestBuilders = { // // NB: options is not required: it is an optimisation shortcut module.exports = { - generate: (type, filters, extensions) => { + generate: (type, filters, extensions, offline) => { + const freetextDdocName = offline ? 'medic-offline-freetext' : 'medic-client'; const builder = requestBuilders[type]; if (!builder) { throw new Error('Unknown type: ' + type); } - return builder(filters, extensions); + return builder(filters, freetextDdocName, extensions); }, shouldSortByLastVisitedDate: (extensions) => { return Boolean(extensions?.sortByLastVisitedDate); diff --git a/shared-libs/search/src/search.js b/shared-libs/search/src/search.js index 69855223e1b..2168ea8fabd 100644 --- a/shared-libs/search/src/search.js +++ b/shared-libs/search/src/search.js @@ -9,6 +9,11 @@ _.flatten = require('lodash/flatten'); _.intersection = require('lodash/intersection'); const GenerateSearchRequests = require('./generate-search-requests'); +const ddocExists = (db, ddocId) => db + .get(ddocId) + .then(() => true) + .catch(() => false); + module.exports = function(Promise, DB) { // Get the subset of rows, in appropriate order, according to options. const getPageRows = function(type, rows, options) { @@ -111,17 +116,18 @@ module.exports = function(Promise, DB) { }); }; - return function(type, filters, options, extensions) { + return async (type, filters, options, extensions) => { options = options || {}; _.defaults(options, { limit: 50, skip: 0 }); + const offline = await ddocExists(DB, '_design/medic-offline-freetext'); const cacheQueryResults = GenerateSearchRequests.shouldSortByLastVisitedDate(extensions); let requests; try { - requests = GenerateSearchRequests.generate(type, filters, extensions); + requests = GenerateSearchRequests.generate(type, filters, extensions, offline); } catch (err) { return Promise.reject(err); } diff --git a/shared-libs/search/test/search.js b/shared-libs/search/test/search.js index 4c296ee8f5f..bc26439dd12 100644 --- a/shared-libs/search/test/search.js +++ b/shared-libs/search/test/search.js @@ -14,7 +14,8 @@ describe('Search service', function() { GenerateSearchRequests.generate = sinon.stub(); GenerateSearchRequests.shouldSortByLastVisitedDate = sinon.stub(); DB = { - query: sinon.stub() + query: sinon.stub(), + get: sinon.stub().rejects() }; service = Search(Promise, DB); diff --git a/tests/e2e/default/contacts/search-contacts.wdio-spec.js b/tests/e2e/default/contacts/search-contacts.wdio-spec.js index 71c81193f63..c07b9aef773 100644 --- a/tests/e2e/default/contacts/search-contacts.wdio-spec.js +++ b/tests/e2e/default/contacts/search-contacts.wdio-spec.js @@ -5,63 +5,101 @@ const contactPage = require('@page-objects/default/contacts/contacts.wdio.page') const commonPage = require('@page-objects/default/common/common.wdio.page'); const placeFactory = require('@factories/cht/contacts/place'); const personFactory = require('@factories/cht/contacts/person'); +const userFactory = require('@factories/cht/users/users'); describe('Contact Search', () => { const places = placeFactory.generateHierarchy(); + const districtHospitalId = places.get('district_hospital')._id; - const sittuHospital = placeFactory.place().build({ - name: 'Sittu Hospital', - type: 'district_hospital', - parent: { _id: '', parent: { _id: '' } } + const sittuHealthCenter = placeFactory.place().build({ + name: 'Sittu Health Center', + type: 'health_center', + parent: { _id: districtHospitalId, parent: { _id: '' } } }); - const potuHospital = placeFactory.place().build({ - name: 'Potu Hospital', - type: 'district_hospital', - parent: { _id: '', parent: { _id: '' } } + const potuHealthCenter = placeFactory.place().build({ + name: 'Potu Health Center', + type: 'health_center', + parent: { _id: districtHospitalId, parent: { _id: '' } } }); const sittuPerson = personFactory.build({ name: 'Sittu', - parent: { _id: sittuHospital._id, parent: sittuHospital.parent } + parent: { _id: sittuHealthCenter._id, parent: sittuHealthCenter.parent } }); const potuPerson = personFactory.build({ name: 'Potu', - parent: { _id: sittuHospital._id, parent: sittuHospital.parent } + parent: { _id: sittuHealthCenter._id, parent: sittuHealthCenter.parent } }); - before(async () => { - await utils.saveDocs([...places.values(), sittuHospital, sittuPerson, potuHospital, potuPerson]); - await loginPage.cookieLogin(); - await commonPage.goToPeople(); + const supervisorPerson = personFactory.build({ + name: 'Supervisor', + parent: { _id: districtHospitalId } }); - it('search by NON empty string should display results with contains match and clears search', async () => { - await contactPage.getAllLHSContactsNames(); - - await searchPage.performSearch('sittu'); - expect(await contactPage.getAllLHSContactsNames()).to.have.members([ - sittuPerson.name, - sittuHospital.name, - ]); + const offlineUser = userFactory.build({ + username: 'offline-search-user', + place: districtHospitalId, + roles: ['chw_supervisor'], + contact: supervisorPerson._id + }); + const onlineUser = userFactory.build({ + username: 'online-search-user', + place: districtHospitalId, + roles: ['program_officer'], + contact: supervisorPerson._id + }); - await searchPage.clearSearch(); - expect(await contactPage.getAllLHSContactsNames()).to.have.members([ - potuHospital.name, - sittuHospital.name, - places.get('district_hospital').name, + before(async () => { + await utils.saveDocs([ + ...places.values(), sittuHealthCenter, sittuPerson, potuHealthCenter, potuPerson, supervisorPerson ]); + await utils.createUsers([offlineUser, onlineUser]); }); - it('search should clear RHS selected contact', async () => { - await contactPage.selectLHSRowByText(potuHospital.name, false); - await contactPage.waitForContactLoaded(); - expect(await (await contactPage.contactCardSelectors.contactCardName()).getText()).to.equal(potuHospital.name); + after(() => utils.deleteUsers([offlineUser, onlineUser])); - await searchPage.performSearch('sittu'); - await contactPage.waitForContactUnloaded(); - const url = await browser.getUrl(); - expect(url.endsWith('/contacts')).to.equal(true); - }); + [ + ['online', onlineUser], + ['offline', offlineUser], + ].forEach(([userType, user]) => describe(`Logged in as an ${userType} user`, () => { + before(async () => { + await loginPage.login(user); + await commonPage.goToPeople(); + }); + + after(commonPage.logout); + + it('search by NON empty string should display results with contains match and clears search', async () => { + await contactPage.getAllLHSContactsNames(); + + await searchPage.performSearch('sittu'); + expect(await contactPage.getAllLHSContactsNames()).to.have.members([ + sittuPerson.name, + sittuHealthCenter.name, + ]); + + await searchPage.clearSearch(); + expect(await contactPage.getAllLHSContactsNames()).to.have.members([ + potuHealthCenter.name, + sittuHealthCenter.name, + places.get('district_hospital').name, + places.get('health_center').name, + ]); + }); + + it('search should clear RHS selected contact', async () => { + await contactPage.selectLHSRowByText(potuHealthCenter.name, false); + await contactPage.waitForContactLoaded(); + expect( + await (await contactPage.contactCardSelectors.contactCardName()).getText() + ).to.equal(potuHealthCenter.name); + + await searchPage.performSearch('sittu'); + await contactPage.waitForContactUnloaded(); + const url = await browser.getUrl(); + expect(url.endsWith('/contacts')).to.equal(true); + }); + })); }); diff --git a/tests/e2e/default/db/initial-replication.wdio-spec.js b/tests/e2e/default/db/initial-replication.wdio-spec.js index 92a907973d7..cc9d7db8b16 100644 --- a/tests/e2e/default/db/initial-replication.wdio-spec.js +++ b/tests/e2e/default/db/initial-replication.wdio-spec.js @@ -6,6 +6,8 @@ const commonPage = require('@page-objects/default/common/common.wdio.page'); const loginPage = require('@page-objects/default/login/login.wdio.page'); const dataFactory = require('@factories/cht/generate'); +const LOCAL_ONLY_DOC_IDS = ['_design/medic-offline-freetext']; + describe('initial-replication', () => { const LOCAL_LOG = '_local/initial-replication'; @@ -52,11 +54,11 @@ describe('initial-replication', () => { await commonPage.sync(false, 7000); - const localAllDocs = await chtDbUtils.getDocs(); + const localAllDocs = (await chtDbUtils.getDocs()).filter(doc => !LOCAL_ONLY_DOC_IDS.includes(doc.id)); const localDocIds = dataFactory.ids(localAllDocs); // no additional docs to download - expect(docIdsPreSync).to.have.members(localDocIds); + expect(docIdsPreSync).to.have.members([...localDocIds, ...LOCAL_ONLY_DOC_IDS]); const serverAllDocs = await getServerDocs(localDocIds); diff --git a/tests/e2e/default/enketo/db-object-widget.wdio-spec.js b/tests/e2e/default/enketo/db-object-widget.wdio-spec.js index 78614392c66..f2109a6eef8 100644 --- a/tests/e2e/default/enketo/db-object-widget.wdio-spec.js +++ b/tests/e2e/default/enketo/db-object-widget.wdio-spec.js @@ -18,81 +18,107 @@ describe('DB Object Widget', () => { parent: { _id: districtHospital._id } }); - const offlineUser = userFactory.build({ place: districtHospital._id, roles: [ 'chw' ] }); + const offlineUser = userFactory.build({ + username: 'offline-db-object-widget-user', + place: districtHospital._id, + roles: [ 'chw' ] + }); offlineUser.contact.sex = 'female'; - const personArea1 = personFactory.build({ parent: { _id: area1._id, parent: area1.parent } }); - const personArea2 = personFactory.build({ name: 'Patricio', parent: { _id: area2._id, parent: area2.parent } }); + offlineUser.contact.name = 'offline user pat'; + const onlineUser = userFactory.build({ + username: 'online-db-object-widget-user', + place: districtHospital._id, + roles: [ 'program_officer' ], + contact: offlineUser.contact._id + }); + const personArea1 = personFactory.build({ name: 'Patricio1', parent: { _id: area1._id, parent: area1.parent } }); + const personArea2 = personFactory.build({ name: 'Patricio2', parent: { _id: area2._id, parent: area2.parent } }); before(async () => { await utils.saveDocIfNotExists(commonPage.createFormDoc(`${__dirname}/forms/db-object-form`)); await utils.saveDocs([ ...places.values(), area2, personArea1, personArea2 ]); - await utils.createUsers([ offlineUser ]); - await loginPage.login(offlineUser); + await utils.createUsers([ offlineUser, onlineUser ]); }); - it('should load contacts in non-relevant inputs group and from calculations', async () => { - await commonPage.goToReports(); - await commonPage.openFastActionReport('db-object-form', false); + after(() => utils.deleteUsers([offlineUser, onlineUser])); + + [ + ['online', onlineUser], + ['offline', offlineUser], + ].forEach(([userType, user]) => describe(`Logged in as an ${userType} user`, () => { + before(async () => { + await loginPage.login(user); + }); - await genericForm.submitForm(); + after(commonPage.logout); - const reportId = await reportsPage.getCurrentReportId(); - await commonPage.sync(); - const { fields } = await utils.getDoc(reportId); - expect(fields).excluding(['meta']).to.deep.equal({ - inputs: { - meta: { location: { lat: '', long: '', error: '', message: '' } }, - user: { - contact_id: offlineUser.contact._id, - name: offlineUser.contact.name, - sex: offlineUser.contact.sex - }, - user_contact: { - _id: offlineUser.contact._id, - name: offlineUser.contact.name, - sex: offlineUser.contact.sex + it('should load contacts in non-relevant inputs group and from calculations', async () => { + await commonPage.goToReports(); + await commonPage.openFastActionReport('db-object-form', false); + + await genericForm.submitForm(); + + const reportId = await reportsPage.getCurrentReportId(); + if (userType === 'offline') { + await commonPage.sync(); + } + const { fields } = await utils.getDoc(reportId); + expect(fields).excluding(['meta']).to.deep.equal({ + inputs: { + meta: { location: { lat: '', long: '', error: '', message: '' } }, + user: { + contact_id: offlineUser.contact._id, + name: offlineUser.contact.name, + sex: offlineUser.contact.sex + }, + user_contact: { + _id: offlineUser.contact._id, + name: offlineUser.contact.name, + sex: offlineUser.contact.sex + }, }, - }, - people: { - user_contact: { - _id: offlineUser.contact._id, - name: offlineUser.contact.name, - sex: offlineUser.contact.sex + people: { + user_contact: { + _id: offlineUser.contact._id, + name: offlineUser.contact.name, + sex: offlineUser.contact.sex + }, + person_test_same_parent: '', + person_test_all: '' }, - person_test_same_parent: '', - person_test_all: '' - }, + }); }); - }); - it('should display only the contacts from the parent contact', async () => { - await commonPage.goToPeople(area1._id); - await commonPage.openFastActionReport('db-object-form'); + it('should display only the contacts from the parent contact', async () => { + await commonPage.goToPeople(area1._id); + await commonPage.openFastActionReport('db-object-form'); - const sameParent = await genericForm.getDBObjectWidgetValues('/db-object-form/people/person_test_same_parent'); - await sameParent[0].click(); - expect(sameParent.length).to.equal(1); - expect(sameParent[0].name).to.equal(personArea1.name); + await genericForm.searchContact('Select a person from the same parent', 'pat'); + const sameParent = await genericForm.getDBObjectWidgetValues(); + await sameParent[0].click(); + expect(sameParent.length).to.equal(1); + expect(sameParent[0].name).to.equal(personArea1.name); - const allContacts = await genericForm.getDBObjectWidgetValues('/db-object-form/people/person_test_all'); - await allContacts[2].click(); - expect(allContacts.length).to.equal(3); - expect(allContacts[0].name).to.equal(personArea1.name); - expect(allContacts[1].name).to.equal(offlineUser.contact.name); - expect(allContacts[2].name).to.equal(personArea2.name); + await genericForm.searchContact('Select a person from all', 'pat'); + const allContacts = await genericForm.getDBObjectWidgetValues(); + await allContacts[1].click(); + expect(allContacts.length).to.equal(3); + expect(allContacts[0].name).to.equal(personArea1.name); + expect(allContacts[1].name).to.equal(personArea2.name); + expect(allContacts[2].name).to.equal(offlineUser.contact.name); - await genericForm.submitForm(); - await commonPage.goToReports(); + await genericForm.submitForm(); + await commonPage.goToReports(); - const firstReport = await reportsPage.getListReportInfo(await reportsPage.leftPanelSelectors.firstReport()); - expect(firstReport.heading).to.equal(offlineUser.contact.name); - expect(firstReport.form).to.equal('db-object-form'); - - await reportsPage.openReport(firstReport.dataId); - expect((await reportsPage.getDetailReportRowContent('report.db-object-form.people.person_test_same_parent')) - .rowValues[0]).to.equal(personArea1._id); - expect((await reportsPage.getDetailReportRowContent('report.db-object-form.people.person_test_all')) - .rowValues[0]).to.equal(personArea2._id); - }); + const firstReport = await reportsPage.getListReportInfo(await reportsPage.leftPanelSelectors.firstReport()); + expect(firstReport.heading).to.equal(offlineUser.contact.name); + expect(firstReport.form).to.equal('db-object-form'); + await reportsPage.openReport(firstReport.dataId); + expect((await reportsPage.getDetailReportRowContent('report.db-object-form.people.person_test_same_parent')) + .rowValues[0]).to.equal(personArea1._id); + expect((await reportsPage.getDetailReportRowContent('report.db-object-form.people.person_test_all')) + .rowValues[0]).to.equal(personArea2._id); + }); + })); }); diff --git a/tests/e2e/default/reports/search-reports.wdio-spec.js b/tests/e2e/default/reports/search-reports.wdio-spec.js index 4b4327bb807..d6b1b8ed2a6 100644 --- a/tests/e2e/default/reports/search-reports.wdio-spec.js +++ b/tests/e2e/default/reports/search-reports.wdio-spec.js @@ -7,11 +7,15 @@ const placeFactory = require('@factories/cht/contacts/place'); const personFactory = require('@factories/cht/contacts/person'); const pregnancyFactory = require('@factories/cht/reports/pregnancy'); const smsPregnancyFactory = require('@factories/cht/reports/sms-pregnancy'); +const userFactory = require('@factories/cht/users/users'); describe('Reports Search', () => { - let reportDocs; const sittuHospital = placeFactory.place().build({ name: 'Sittu Hospital', type: 'district_hospital' }); - const potuHospital = placeFactory.place().build({ name: 'Potu Hospital', type: 'district_hospital' }); + const potuHealthCenter = placeFactory.place().build({ + name: 'Potu Health Center', + type: 'health_center', + parent: { _id: sittuHospital._id } + }); const sittuPerson = personFactory.build({ name: 'Sittu', @@ -21,7 +25,7 @@ describe('Reports Search', () => { const potuPerson = personFactory.build({ name: 'Potu', patient_id: 'potu-patient', - parent: { _id: potuHospital._id, parent: potuHospital.parent }, + parent: { _id: potuHealthCenter._id, parent: potuHealthCenter.parent }, }); const reports = [ @@ -31,44 +35,67 @@ describe('Reports Search', () => { pregnancyFactory.build({ fields: { patient_id: potuPerson.patient_id, case_id: 'case-12' } }), ]; + const offlineUser = userFactory.build({ + username: 'offline-search-user', + place: sittuHospital._id, + roles: ['chw_supervisor'], + contact: sittuPerson._id + }); + const onlineUser = userFactory.build({ + username: 'online-search-user', + place: sittuHospital._id, + roles: ['program_officer'], + contact: sittuPerson._id + }); + before(async () => { - await utils.saveDocs([ sittuHospital, sittuPerson, potuHospital, potuPerson ]); - reportDocs = await utils.saveDocs(reports); - await loginPage.cookieLogin(); + await utils.saveDocs([ sittuHospital, sittuPerson, potuHealthCenter, potuPerson ]); + await utils.saveDocs(reports); + await utils.createUsers([offlineUser, onlineUser]); }); - it('should return results matching the search term and then return all data when clearing search', async () => { - const [ sittuSMSPregnancy, potuSMSPregnancy, sittuPregnancy, potuPregnancy ] = reportDocs; - await commonPage.goToReports(); - // Asserting first load reports - expect((await reportsPage.reportsListDetails()).length).to.equal(reportDocs.length); + after(() => utils.deleteUsers([offlineUser, onlineUser])); - await searchPage.performSearch('sittu'); - await commonPage.waitForLoaders(); - expect((await reportsPage.reportsListDetails()).length).to.equal(2); - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(sittuSMSPregnancy.id)).isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(sittuPregnancy.id)).isDisplayed()).to.be.true; + [ + ['online', onlineUser, [reports[0], reports[2]], reports], + ['offline', offlineUser, [reports[2]], [reports[2], reports[3]]], + ].forEach(([userType, user, filteredReports, allReports]) => describe(`Logged in as an ${userType} user`, () => { + before(() => loginPage.login(user)); - await searchPage.clearSearch(); - expect((await reportsPage.reportsListDetails()).length).to.equal(reportDocs.length); - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(sittuSMSPregnancy.id)).isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(potuSMSPregnancy.id)).isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(sittuPregnancy.id)).isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(potuPregnancy.id)).isDisplayed()).to.be.true; - }); + after(commonPage.logout); - it('should return results when searching by case_id', async () => { - const sittuPregnancy = reportDocs[2]; - const potuPregnancy = reportDocs[3]; - await commonPage.goToReports(); - // Asserting first load reports - expect((await reportsPage.reportsListDetails()).length).to.equal(reportDocs.length); + it('should return results matching the search term and then return all data when clearing search', async () => { + await commonPage.goToReports(); + // Asserting first load reports + expect((await reportsPage.reportsListDetails()).length).to.equal(allReports.length); - await reportsPage.openReport(sittuPregnancy.id); - await reportsPage.clickOnCaseId(); - await commonPage.waitForLoaders(); - expect((await reportsPage.reportsListDetails()).length).to.equal(2); - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(sittuPregnancy.id)).isDisplayed()).to.be.true; - expect(await (await reportsPage.leftPanelSelectors.reportByUUID(potuPregnancy.id)).isDisplayed()).to.be.true; - }); + await searchPage.performSearch('sittu'); + await commonPage.waitForLoaders(); + expect((await reportsPage.reportsListDetails()).length).to.equal(filteredReports.length); + for (const report of filteredReports) { + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(report._id)).isDisplayed()).to.be.true; + } + + await searchPage.clearSearch(); + expect((await reportsPage.reportsListDetails()).length).to.equal(allReports.length); + for (const report of allReports) { + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(report._id)).isDisplayed()).to.be.true; + } + }); + + it('should return results when searching by case_id', async () => { + const sittuPregnancy = reports[2]; + const potuPregnancy = reports[3]; + await commonPage.goToReports(); + // Asserting first load reports + expect((await reportsPage.reportsListDetails()).length).to.equal(allReports.length); + + await reportsPage.openReport(sittuPregnancy._id); + await reportsPage.clickOnCaseId(); + await commonPage.waitForLoaders(); + expect((await reportsPage.reportsListDetails()).length).to.equal(2); + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(sittuPregnancy._id)).isDisplayed()).to.be.true; + expect(await (await reportsPage.leftPanelSelectors.reportByUUID(potuPregnancy._id)).isDisplayed()).to.be.true; + }); + })); }); diff --git a/tests/page-objects/default/enketo/generic-form.wdio.page.js b/tests/page-objects/default/enketo/generic-form.wdio.page.js index 7eed056b39f..32e18f4d5ef 100644 --- a/tests/page-objects/default/enketo/generic-form.wdio.page.js +++ b/tests/page-objects/default/enketo/generic-form.wdio.page.js @@ -30,14 +30,18 @@ const nextPage = async (numberOfPages = 1, waitForLoad = true) => { } }; -const selectContact = async (contactName, label, searchTerm = '') => { +const searchContact = async (label, searchTerm) => { const searchField = await $('.select2-search__field'); if (!await searchField.isDisplayed()) { await (await select2Selection(label)).click(); } - await searchField.setValue(searchTerm || contactName); + await searchField.setValue(searchTerm); await $('.select2-results__option.loading-results').waitForDisplayed({ reverse: true }); +}; + +const selectContact = async (contactName, label, searchTerm = '') => { + await searchContact(label, searchTerm || contactName); const contact = await $(`.name*=${contactName}`); await contact.waitForDisplayed(); await contact.click(); @@ -78,11 +82,7 @@ const getFormTitle = async () => { return await (await formTitle()).getText(); }; -const getDBObjectWidgetValues = async (field) => { - const widget = $(`[data-contains-ref-target="${field}"] .selection`); - await (await widget).waitForClickable(); - await (await widget).click(); - +const getDBObjectWidgetValues = async () => { const dropdown = $('.select2-dropdown--below'); await (await dropdown).waitForDisplayed(); const firstElement = $('.select2-results__options > li'); @@ -91,8 +91,12 @@ const getDBObjectWidgetValues = async (field) => { const list = await $$('.select2-results__options > li'); const contacts = []; for (const item of list) { + const itemName = item.$('.name'); + if (!(await itemName.isExisting())) { + continue; + } contacts.push({ - name: await (item.$('.name').getText()), + name: await itemName.getText(), click: () => item.click(), }); } @@ -109,6 +113,7 @@ module.exports = { nextPage, nameField, fieldByName, + searchContact, selectContact, clearSelectedContact, cancelForm, diff --git a/webapp/src/js/bootstrapper/index.js b/webapp/src/js/bootstrapper/index.js index f0136478a51..dde8331f2a0 100644 --- a/webapp/src/js/bootstrapper/index.js +++ b/webapp/src/js/bootstrapper/index.js @@ -7,6 +7,7 @@ const utils = require('./utils'); const purger = require('./purger'); const initialReplicationLib = require('./initial-replication'); + const offlineDdocs = require('./offline-ddocs'); const ONLINE_ROLE = 'mm-online'; @@ -67,6 +68,15 @@ window.location.href = '/' + dbInfo.name + '/login?redirect=' + currentUrl; }; + const handleBootstrappingError = (err, dbInfo) => { + const errorCode = err.status || err.code; + if (errorCode === 401) { + return redirectToLogin(dbInfo); + } + setUiError(err); + throw (err); + }; + // TODO Use a shared library for this duplicated code #4021 const hasRole = function(userCtx, role) { if (userCtx.roles) { @@ -83,8 +93,18 @@ return hasRole(userCtx, '_admin') || hasRole(userCtx, ONLINE_ROLE); }; + const doInitialReplication = async (remoteDb, localDb, userCtx) => { + const replicationStarted = performance.now(); + // Polling the document count from the db. + await initialReplicationLib.replicate(remoteDb, localDb); + if (await initialReplicationLib.isReplicationNeeded(localDb, userCtx)) { + throw new Error('Initial replication failed'); + } + window.startupTimes.replication = performance.now() - replicationStarted; + }; + /* pouch db set up function */ - module.exports = (POUCHDB_OPTIONS) => { + module.exports = async (POUCHDB_OPTIONS) => { const dbInfo = getDbInfo(); const userCtx = getUserCtx(); @@ -108,58 +128,39 @@ const localMetaDb = window.PouchDB(getLocalMetaDbName(dbInfo, userCtx.name), POUCHDB_OPTIONS.local); - return Promise - .all([ - initialReplicationLib.isReplicationNeeded(localDb, userCtx), - swRegistration, - setReplicationId(POUCHDB_OPTIONS, localDb) - ]) - .then(([isInitialReplicationNeeded]) => { - utils.setOptions(POUCHDB_OPTIONS); - - if (isInitialReplicationNeeded) { - const replicationStarted = performance.now(); - // Polling the document count from the db. - return initialReplicationLib - .replicate(remoteDb, localDb) - .then(() => initialReplicationLib.isReplicationNeeded(localDb, userCtx)) - .then(isReplicationStillNeeded => { - if (isReplicationStillNeeded) { - throw new Error('Initial replication failed'); - } - }) - .then(() => window.startupTimes.replication = performance.now() - replicationStarted); - } - }) - .then(() => { - const purgeMetaStarted = performance.now(); - return purger - .purgeMeta(localMetaDb) - .on('should-purge', shouldPurge => window.startupTimes.purgingMeta = shouldPurge) - .on('start', () => setUiStatus('PURGE_META')) - .on('done', () => window.startupTimes.purgeMeta = performance.now() - purgeMetaStarted) - .catch(err => { - console.error('Error attempting to purge meta db - continuing', err); - window.startupTimes.purgingMetaFailed = err.message; - }); - }) - .then(() => setUiStatus('STARTING_APP')) - .catch(err => err) - .then(err => { - localDb.close(); - remoteDb.close(); - localMetaDb.close(); - - if (err) { - const errorCode = err.status || err.code; - if (errorCode === 401) { - return redirectToLogin(dbInfo); - } - setUiError(err); - throw (err); - } - }); + try { + const [isInitialReplicationNeeded] = await Promise + .all([ + initialReplicationLib.isReplicationNeeded(localDb, userCtx), + swRegistration, + setReplicationId(POUCHDB_OPTIONS, localDb), + offlineDdocs.init(localDb) + ]); + + utils.setOptions(POUCHDB_OPTIONS); + + if (isInitialReplicationNeeded) { + await doInitialReplication(remoteDb, localDb, userCtx); + } + const purgeMetaStarted = performance.now(); + await purger + .purgeMeta(localMetaDb) + .on('should-purge', shouldPurge => window.startupTimes.purgingMeta = shouldPurge) + .on('start', () => setUiStatus('PURGE_META')) + .on('done', () => window.startupTimes.purgeMeta = performance.now() - purgeMetaStarted) + .catch(err => { + console.error('Error attempting to purge meta db - continuing', err); + window.startupTimes.purgingMetaFailed = err.message; + }); + + setUiStatus('STARTING_APP'); + } catch (err) { + return handleBootstrappingError(err, dbInfo); + } finally { + localDb.close(); + remoteDb.close(); + localMetaDb.close(); + } }; - }()); diff --git a/webapp/src/js/bootstrapper/offline-ddocs/.eslintrc b/webapp/src/js/bootstrapper/offline-ddocs/.eslintrc new file mode 100644 index 00000000000..15b79d3fd83 --- /dev/null +++ b/webapp/src/js/bootstrapper/offline-ddocs/.eslintrc @@ -0,0 +1,5 @@ +{ + "globals": { + "emit": true + } +} diff --git a/webapp/src/js/bootstrapper/offline-ddocs/index.js b/webapp/src/js/bootstrapper/offline-ddocs/index.js new file mode 100644 index 00000000000..b6cda237df4 --- /dev/null +++ b/webapp/src/js/bootstrapper/offline-ddocs/index.js @@ -0,0 +1,18 @@ +const contactsByFreetext = require('./medic-offline-freetext'); + +const getRev = async (db, id) => db + .get(id) + .then(({ _rev }) => _rev) + .catch((e) => { + if (e.status === 404) { + return undefined; + } + throw e; + }); + +const initDdoc = async (db, ddoc) => db.put({ + ...ddoc, + _rev: await getRev(db, ddoc._id), +}); + +module.exports.init = async (db) => initDdoc(db, contactsByFreetext); diff --git a/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/contacts_by_freetext.js b/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/contacts_by_freetext.js new file mode 100644 index 00000000000..3248d2f8dcb --- /dev/null +++ b/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/contacts_by_freetext.js @@ -0,0 +1,58 @@ +module.exports.map = (doc) => { + const skip = [ '_id', '_rev', 'type', 'refid', 'geolocation' ]; + + const usedKeys = []; + const emitMaybe = (key, value) => { + if (usedKeys.indexOf(key) === -1 && // Not already used + key.length > 2 // Not too short + ) { + usedKeys.push(key); + emit([key], value); + } + }; + + const emitField = (key, value, order) => { + if (!value) { + return; + } + const lowerKey = key.toLowerCase(); + if (skip.indexOf(lowerKey) !== -1 || /_date$/.test(lowerKey)) { + return; + } + if (typeof value === 'string') { + const lowerValue = value.toLowerCase(); + lowerValue + .split(/\s+/) + .forEach((word) => emitMaybe(word, order)); + emitMaybe(`${lowerKey}:${lowerValue}`, order); + } else if (typeof value === 'number') { + emitMaybe(`${lowerKey}:${value}`, order); + } + }; + + const getTypeIndex = () => { + const types = [ 'district_hospital', 'health_center', 'clinic', 'person' ]; + if (doc.type !== 'contact') { + return types.indexOf(doc.type); + } + + const contactTypeIdx = types.indexOf(doc.contact_type); + if (contactTypeIdx >= 0) { + return contactTypeIdx; + } + + return doc.contact_type; + }; + + const idx = getTypeIndex(); + if (idx === -1) { + return; + } + + const dead = !!doc.date_of_death; + const muted = !!doc.muted; + const order = `${dead} ${muted} ${idx} ${(doc.name && doc.name.toLowerCase())}`; + Object + .keys(doc) + .forEach((key) => emitField(key, doc[key], order)); +}; diff --git a/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/contacts_by_type_freetext.js b/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/contacts_by_type_freetext.js new file mode 100644 index 00000000000..689a5669701 --- /dev/null +++ b/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/contacts_by_type_freetext.js @@ -0,0 +1,64 @@ +module.exports.map = (doc) => { + const skip = [ '_id', '_rev', 'type', 'refid', 'geolocation' ]; + const keyShouldBeSkipped = key => skip.indexOf(key) !== -1 || /_date$/.test(key); + + const usedKeys = []; + const emitMaybe = function(type, key, value) { + if (usedKeys.indexOf(key) === -1 && // Not already used + key.length > 2 // Not too short + ) { + usedKeys.push(key); + emit([ type, key ], value); + } + }; + + const emitField = (type, key, value, order) => { + if (!key || !value) { + return; + } + const lowerKey = key.toLowerCase(); + if (keyShouldBeSkipped(lowerKey)) { + return; + } + if (typeof value === 'string') { + const lowerValue = value.toLowerCase(); + lowerValue + .split(/\s+/) + .forEach(word => emitMaybe(type, word, order)); + emitMaybe(type, `${lowerKey}:${lowerValue}`, order); + } else if (typeof value === 'number') { + emitMaybe(type, `${lowerKey}:${value}`, order); + } + }; + + const getType = () => { + if (doc.type !== 'contact') { + return doc.type; + } + + return doc.contact_type; + }; + + const getTypeIndex = type => { + const types = [ 'district_hospital', 'health_center', 'clinic', 'person' ]; + const typeIndex = types.indexOf(type); + if (typeIndex === -1 && doc.type === 'contact') { + return type; + } + + return typeIndex; + }; + + const type = getType(); + const idx = getTypeIndex(type); + if (idx === -1) { + return; + } + + const dead = !!doc.date_of_death; + const muted = !!doc.muted; + const order = `${dead} ${muted} ${idx} ${doc.name && doc.name.toLowerCase()}`; + Object + .keys(doc) + .forEach(key => emitField(type, key, doc[key], order)); +}; diff --git a/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/index.js b/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/index.js new file mode 100644 index 00000000000..67bc037eb73 --- /dev/null +++ b/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/index.js @@ -0,0 +1,14 @@ +const contactByFreetext = require('./contacts_by_freetext'); +const contactsByTypeFreetext = require('./contacts_by_type_freetext'); +const reportsByFreetext = require('./reports_by_freetext'); + +const packageView = ({ map }) => ({ map: map.toString() }); + +module.exports = { + _id: '_design/medic-offline-freetext', + views: { + contacts_by_freetext: packageView(contactByFreetext), + contacts_by_type_freetext: packageView(contactsByTypeFreetext), + reports_by_freetext: packageView(reportsByFreetext), + } +}; diff --git a/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/reports_by_freetext.js b/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/reports_by_freetext.js new file mode 100644 index 00000000000..d3656176c75 --- /dev/null +++ b/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-freetext/reports_by_freetext.js @@ -0,0 +1,49 @@ +module.exports.map = (doc) => { + const skip = [ '_id', '_rev', 'type', 'refid', 'content' ]; + const keyShouldBeSkipped = key => skip.indexOf(key) !== -1 || /_date$/.test(key); + + const usedKeys = []; + const emitMaybe = (key, value) => { + if (usedKeys.indexOf(key) === -1 && // Not already used + key.length > 2 // Not too short + ) { + usedKeys.push(key); + emit([key], value); + } + }; + + const emitField = (key, value, reportedDate) => { + if (!key || !value) { + return; + } + const lowerKey = key.toLowerCase(); + if (keyShouldBeSkipped(lowerKey)) { + return; + } + if (typeof value === 'string') { + const lowerValue = value.toLowerCase(); + lowerValue + .split(/\s+/) + .forEach((word) => emitMaybe(word, reportedDate)); + emitMaybe(`${lowerKey}:${lowerValue}`, reportedDate); + } else if (typeof value === 'number') { + emitMaybe(`${lowerKey}:${value}`, reportedDate); + } + }; + + if (doc.type !== 'data_record' || !doc.form) { + return; + } + + Object + .keys(doc) + .forEach((key) => emitField(key, doc[key], doc.reported_date)); + if (doc.fields) { + Object + .keys(doc.fields) + .forEach((key) => emitField(key, doc.fields[key], doc.reported_date)); + } + if (doc.contact && doc.contact._id) { + emitMaybe(`contact:${doc.contact._id.toLowerCase()}`, doc.reported_date); + } +}; diff --git a/webapp/tests/mocha/unit/bootstrapper.spec.js b/webapp/tests/mocha/unit/bootstrapper.spec.js index d3cf5580b54..b587bff5b86 100644 --- a/webapp/tests/mocha/unit/bootstrapper.spec.js +++ b/webapp/tests/mocha/unit/bootstrapper.spec.js @@ -11,6 +11,7 @@ const bootstrapper = rewire('../../../src/js/bootstrapper'); const purger = require('../../../src/js/bootstrapper/purger'); const utils = require('../../../src/js/bootstrapper/utils'); const initialReplication = require('../../../src/js/bootstrapper/initial-replication'); +const offlineDdocs = require('../../../src/js/bootstrapper/offline-ddocs'); let originalDocument; let originalWindow; @@ -25,6 +26,7 @@ let localAllDocs; let localId; let purgeOn; let localMetaClose; +let offlineDdocsInit; let localMedicDb; let remoteMedicDb; @@ -41,6 +43,7 @@ describe('bootstrapper', () => { localAllDocs = sinon.stub(); localId = sinon.stub().resolves(); localMetaClose = sinon.stub(); + offlineDdocsInit = sinon.stub(offlineDdocs, 'init').resolves(); localMedicDb = { get: localGet, @@ -126,6 +129,7 @@ describe('bootstrapper', () => { setUserCtxCookie({ name: 'jimbo', roles: [ '_admin' ] }); await bootstrapper(pouchDbOptions); assert.equal(pouchDb.callCount, 0); + assert.isTrue(offlineDdocsInit.notCalled); }); it('should initialize replication header with local db id', async () => { @@ -149,6 +153,7 @@ describe('bootstrapper', () => { }); assert.equal(utils.setOptions.callCount, 1); assert.equal(purger.purgeMeta.callCount, 1); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('should initialize purger with correct options', async () => { @@ -163,6 +168,7 @@ describe('bootstrapper', () => { assert.equal(utils.setOptions.callCount, 1); assert.deepEqual(utils.setOptions.args[0], [pouchDbOptions]); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('returns if initial replication is not needed', async () => { @@ -183,6 +189,7 @@ describe('bootstrapper', () => { localMedicDb, { locale: undefined, name: 'jim' }, ]]); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('performs initial replication', async () => { @@ -207,6 +214,7 @@ describe('bootstrapper', () => { ]); expect(initialReplication.replicate.callCount).to.equal(1); expect(initialReplication.replicate.args).to.deep.equal([[ remoteMedicDb, localMedicDb ]]); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('should redirect to login when no userCtx cookie found', async () => { @@ -224,6 +232,7 @@ describe('bootstrapper', () => { window.location.href, '/medic/login?redirect=http%3A%2F%2Flocalhost%3A5988%2Fmedic%2F_design%2Fmedic%2F_rewrite%2F%23%2Fmessages' ); + assert.isTrue(offlineDdocsInit.notCalled); }); it('should redirect to login when initial replication returns unauthorized', async () => { @@ -240,6 +249,7 @@ describe('bootstrapper', () => { window.location.href, '/medic/login?redirect=http%3A%2F%2Flocalhost%3A5988%2Fmedic%2F_design%2Fmedic%2F_rewrite%2F%23%2Fmessages' ); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('returns other errors in initial replication', async () => { @@ -250,6 +260,7 @@ describe('bootstrapper', () => { sinon.stub(initialReplication, 'replicate').rejects(new Error('message')); await expect(bootstrapper(pouchDbOptions)).to.be.rejectedWith(Error, 'message'); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('returns error if initial replication is still needed', async () => { @@ -266,6 +277,7 @@ describe('bootstrapper', () => { assert.equal(remoteClose.callCount, 1); assert.equal(utils.setOptions.callCount, 1); expect(initialReplication.isReplicationNeeded.callCount).to.equal(2); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('error results if service worker fails registration', async () => { @@ -276,6 +288,7 @@ describe('bootstrapper', () => { window.navigator.serviceWorker.register = failingRegister; await expect(bootstrapper(pouchDbOptions)).to.be.rejectedWith(Error, 'redundant'); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('should run meta purge on startup', async () => { @@ -288,6 +301,7 @@ describe('bootstrapper', () => { await bootstrapper(pouchDbOptions); assert.equal(purger.purgeMeta.callCount, 1); + assert.isTrue(offlineDdocsInit.calledOnce); }); it('should catch meta purge errors', async () => { @@ -308,5 +322,6 @@ describe('bootstrapper', () => { assert.equal(purger.purgeMeta.callCount, 1); assert.deepEqual(purger.purgeMeta.args[0], [localMetaDb]); + assert.isTrue(offlineDdocsInit.calledOnce); }); }); diff --git a/webapp/tests/mocha/unit/views/contacts_by_freetext.spec.js b/webapp/tests/mocha/unit/views/contacts_by_freetext.spec.js index 6d5d5e3f1eb..2e057ff488d 100644 --- a/webapp/tests/mocha/unit/views/contacts_by_freetext.spec.js +++ b/webapp/tests/mocha/unit/views/contacts_by_freetext.spec.js @@ -1,95 +1,214 @@ -const _ = require('lodash'); -const assert = require('chai').assert; -const utils = require('./utils'); - -const doc = { - _id: '3c0c4575468bc9b7ce066a279b022e8e', - _rev: '2-5fb6ead9b03232a4cf1e0171c5434469', - name: 'Test Contact of Clinic', - date_of_birth: '', - phone: '+13125551212', - alternate_phone: '', - notes: '', - type: 'person', - reported_date: 1491910934051, - transitions: { - maintain_info_document: { - last_rev: 2, - seq: '241-g1AAAACbeJzLYWBgYMpgTmEQTM4vTc5ISXLIyU9OzMnILy7JAUklMiTV____', - ok: true - } - } -}; - -const nonAsciiDoc = { - _id: '3e32235b-7111-4a69-a0a1-b3094f257891', - _rev: '1-e19cb2355b26c5f71abd1cc67b4b1bc0', - name: 'बुद्ध Élève', - date_of_birth: '', - phone: '+254777444333', - alternate_phone: '', - notes: '', - parent: { - _id: 'd978f02c-093b-4266-81cd-3983749f9c99' - }, - type: 'person', - reported_date: 1496068842996 -}; - -describe('contacts_by_freetext view', () => { - - it('indexes doc name', () => { - // given - const map = utils.loadView('medic-db', 'medic-client', 'contacts_by_freetext'); - - // when - const emitted = map(doc); - - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - const flattened = _.flattenDeep(emitted); - assert.include(flattened, 'test'); - assert.include(flattened, 'clinic'); - assert.include(flattened, 'contact'); +const { loadView, buildViewMapFn } = require('./utils'); +const medicOfflineFreetext = require('../../../../src/js/bootstrapper/offline-ddocs/medic-offline-freetext'); +const { expect } = require('chai'); + +const expectedValue = ( + {typeIndex, name, dead = false, muted = false } = {} +) => `${dead} ${muted} ${typeIndex} ${name}`; + +describe('contacts_by_freetext', () => { + [ + ['online view', loadView('medic-db', 'medic-client', 'contacts_by_freetext')], + ['offline view', buildViewMapFn(medicOfflineFreetext.views.contacts_by_freetext.map)], + ].forEach(([name, mapFn]) => { + describe(name, () => { + afterEach(() => mapFn.reset()); + [ + ['district_hospital', 0], + ['health_center', 1], + ['clinic', 2], + ['person', 3], + ].forEach(([type, typeIndex]) => it('emits numerical index for default type', () => { + const doc = { type, hello: 'world' }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex }); + expect(emitted).to.deep.equal([ + { key: ['world'], value }, + { key: ['hello:world'], value } + ]); + })); + + [ + ['contact', 0, 'district_hospital'], + ['contact', 1, 'health_center'], + ['contact', 2, 'clinic'], + ['contact', 3, 'person'] + ].forEach(([type, typeIndex, contactType]) => it( + 'emits numerical index for default type when used as custom type', + () => { + const doc = { type, hello: 'world', contact_type: contactType }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex }); + expect(emitted).to.deep.equal([ + { key: ['world'], value }, + { key: ['hello:world'], value }, + { key: [contactType], value }, + { key: [`contact_type:${contactType}`], value }, + ]); + } + )); + + it('emits contact_type index for custom type', () => { + const typeIndex = 'my_custom_type'; + const doc = { contact_type: typeIndex, type: 'contact', hello: 'world' }; + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex }); + expect(emitted).to.deep.equal([ + { key: [typeIndex], value }, + { key: [`contact_type:${typeIndex}`], value }, + { key: ['world'], value }, + { key: ['hello:world'], value }, + ]); + }); + + [ + undefined, + 'invalid' + ].forEach(type => it('emits nothing when type is invalid', () => { + const doc = { type, hello: 'world' }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + })); + + it('emits death status in value', () => { + const doc = { type: 'district_hospital', date_of_death: '2021-01-01' }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0, dead: true }); + expect(emitted).to.deep.equal([ + { key: ['2021-01-01'], value }, + { key: ['date_of_death:2021-01-01'], value } + ]); + }); + + it('emits muted status in value', () => { + const doc = { type: 'district_hospital', muted: true, hello: 'world' }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0, muted: true }); + expect(emitted).to.deep.equal([ + { key: ['world'], value }, + { key: ['hello:world'], value } + ]); + }); + + [ + 'hello', 'HeLlO' + ].forEach(name => it('emits name in value', () => { + const doc = { type: 'district_hospital', name }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0, name: name.toLowerCase() }); + expect(emitted).to.deep.equal([ + { key: [name.toLowerCase()], value }, + { key: [`name:${name.toLowerCase()}`], value } + ]); + })); + + [ + null, undefined, { hello: 'world' }, {}, true + ].forEach(hello => it('emits nothing when value is not a string or number', () => { + const doc = { type: 'district_hospital', hello }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + })); + + it('emits only key:value when value is number', () => { + const doc = { type: 'district_hospital', hello: 1234 }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0 }); + expect(emitted).to.deep.equal([{ key: ['hello:1234'], value }]); + }); + + [ + 't', 'to' + ].forEach(hello => it('emits nothing but key:value when value is too short', () => { + const doc = { type: 'district_hospital', hello }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0 }); + expect(emitted).to.deep.equal([{ key: [`hello:${hello}`], value }]); + })); + + it('emits nothing when value is empty', () => { + const doc = { type: 'district_hospital', hello: '' }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + }); + + [ + '_id', '_rev', 'type', 'refid', 'geolocation', 'Refid' + ].forEach(key => it('emits nothing for a skipped field', () => { + const doc = { type: 'district_hospital', [key]: 'world' }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + })); + + it('emits nothing for fields that end with "_date"', () => { + const doc = { type: 'district_hospital', reported_date: 'world' }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + }); + + it('emits value only once', () => { + const doc = { + type: 'district_hospital', + hello: 'world world', + hello1: 'world', + hello3: 'world', + }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0 }); + expect(emitted).to.deep.equal([ + { key: ['world'], value }, + { key: ['hello:world world'], value }, + { key: ['hello1:world'], value }, + { key: ['hello3:world'], value } + ]); + }); + + it('emits each word in a string', () => { + const doc = { + type: 'district_hospital', + hello: `the quick\nBrown\tfox`, + }; + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0 }); + expect(emitted).to.deep.equal([ + { key: ['the'], value }, + { key: ['quick'], value }, + { key: ['brown'], value }, + { key: ['fox'], value }, + { key: ['hello:the quick\nbrown\tfox'], value }, + ]); + }); + + it('emits non-ascii values', () => { + const doc = { type: 'district_hospital', name: 'बुद्ध Élève' }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0, name: 'बुद्ध élève' }); + expect(emitted).to.deep.equal([ + { key: ['बुद्ध'], value }, + { key: ['élève'], value }, + { key: ['name:बुद्ध élève'], value } + ]); + }); + }); }); - - it('indexes non-ascii doc name', () => { - // given - const map = utils.loadView('medic-db', 'medic-client', 'contacts_by_freetext'); - - // when - const emitted = map(nonAsciiDoc); - - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - const flattened = _.flattenDeep(emitted); - assert.include(flattened, 'बुद्ध'); - assert.include(flattened, 'élève'); - }); - - it('does not index words of less than 3 chars', () => { - // given - const map = utils.loadView('medic-db', 'medic-client', 'contacts_by_freetext'); - - // when - const emitted = map(doc); - - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - const flattened = _.flattenDeep(emitted); - assert.notInclude(flattened, 'of'); - }); - - it('does not index non-contact docs', () => { - // given - const map = utils.loadView('medic-db', 'medic-client', 'contacts_by_freetext'); - - // when - const emitted = map({ type: 'data_record', name: 'do not index me'}); - - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - assert.equal(emitted.length, 0); - }); - }); diff --git a/webapp/tests/mocha/unit/views/contacts_by_type_freetext.spec.js b/webapp/tests/mocha/unit/views/contacts_by_type_freetext.spec.js index 4384080ba94..42b0694364d 100644 --- a/webapp/tests/mocha/unit/views/contacts_by_type_freetext.spec.js +++ b/webapp/tests/mocha/unit/views/contacts_by_type_freetext.spec.js @@ -1,104 +1,214 @@ -const assert = require('chai').assert; -const utils = require('./utils'); - -const doc = { - _id: '3c0c4575468bc9b7ce066a279b022e8e', - _rev: '2-5fb6ead9b03232a4cf1e0171c5434469', - name: 'Test Contact of Clinic', - date_of_birth: '', - phone: '+13125551212', - alternate_phone: '', - notes: '', - type: 'person', - reported_date: 1491910934051, - transitions: { - maintain_info_document: { - last_rev: 2, - seq: '241-g1AAAACbeJzLYWBgYMpgTmEQTM4vTc5ISXLIyU9OzMnILy7JAUklMiTV____', - ok: true - } - } -}; - -const nonAsciiDoc = { - _id: '3e32235b-7111-4a69-a0a1-b3094f257891', - _rev: '1-e19cb2355b26c5f71abd1cc67b4b1bc0', - name: 'बुद्ध Élève', - date_of_birth: '', - phone: '+254777444333', - alternate_phone: '', - notes: '', - parent: { - _id: 'd978f02c-093b-4266-81cd-3983749f9c99' - }, - type: 'person', - reported_date: 1496068842996 -}; - -const configurableHierarchyDoc = { - _id: '3e32235b-7111-4a69-a0a1-b3094f257892', - _rev: '1-e19cb2355b26c5f71abd1cc67b4b1bc0', - name: 'jessie', - date_of_birth: '', - phone: '+254777444333', - alternate_phone: '', - notes: '', - parent: { - _id: 'd978f02c-093b-4266-81cd-3983749f9c99' - }, - type: 'contact', - contact_type: 'chp', - reported_date: 1496068842996 -}; - -let map; - -describe('contacts_by_type_freetext view', () => { - - beforeEach(() => map = utils.loadView('medic-db', 'medic-client', 'contacts_by_type_freetext')); - - it('indexes doc name and type', () => { - // when - const emitted = map(doc); - - // then - utils.assertIncludesPair(emitted, ['person', 'test']); - utils.assertIncludesPair(emitted, ['person', 'clinic']); - utils.assertIncludesPair(emitted, ['person', 'contact']); +const { loadView, buildViewMapFn } = require('./utils'); +const medicOfflineFreetext = require('../../../../src/js/bootstrapper/offline-ddocs/medic-offline-freetext'); +const { expect } = require('chai'); + +const expectedValue = ( + {typeIndex, name, dead = false, muted = false } = {} +) => `${dead} ${muted} ${typeIndex} ${name}`; + +describe('contacts_by_type_freetext', () => { + [ + ['online view', loadView('medic-db', 'medic-client', 'contacts_by_type_freetext')], + ['offline view', buildViewMapFn(medicOfflineFreetext.views.contacts_by_type_freetext.map)], + ].forEach(([name, mapFn]) => { + describe(name, () => { + afterEach(() => mapFn.reset()); + [ + ['district_hospital', 0], + ['health_center', 1], + ['clinic', 2], + ['person', 3], + ].forEach(([type, typeIndex]) => it('emits numerical index for default type', () => { + const doc = { type, hello: 'world' }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex }); + expect(emitted).to.deep.equal([ + { key: [type, 'world'], value }, + { key: [type, 'hello:world'], value } + ]); + })); + + [ + ['contact', 0, 'district_hospital'], + ['contact', 1, 'health_center'], + ['contact', 2, 'clinic'], + ['contact', 3, 'person'] + ].forEach(([type, typeIndex, contactType]) => it( + 'emits numerical index for default type when used as custom type', + () => { + const doc = { type, hello: 'world', contact_type: contactType }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex }); + expect(emitted).to.deep.equal([ + { key: [contactType, 'world'], value }, + { key: [contactType, 'hello:world'], value }, + { key: [contactType, contactType], value }, + { key: [contactType, `contact_type:${contactType}`], value }, + ]); + } + )); + + it('emits contact_type index for custom type', () => { + const typeIndex = 'my_custom_type'; + const doc = { contact_type: typeIndex, type: 'contact', hello: 'world' }; + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex }); + expect(emitted).to.deep.equal([ + { key: [typeIndex, typeIndex], value }, + { key: [typeIndex, `contact_type:${typeIndex}`], value }, + { key: [typeIndex, 'world'], value }, + { key: [typeIndex, 'hello:world'], value }, + ]); + }); + + [ + undefined, + 'invalid' + ].forEach(type => it('emits nothing when type is invalid', () => { + const doc = { type, hello: 'world' }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + })); + + it('emits death status in value', () => { + const doc = { type: 'district_hospital', date_of_death: '2021-01-01' }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0, dead: true }); + expect(emitted).to.deep.equal([ + { key: ['district_hospital', '2021-01-01'], value }, + { key: ['district_hospital', 'date_of_death:2021-01-01'], value } + ]); + }); + + it('emits muted status in value', () => { + const doc = { type: 'district_hospital', muted: true, hello: 'world' }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0, muted: true }); + expect(emitted).to.deep.equal([ + { key: ['district_hospital', 'world'], value }, + { key: ['district_hospital', 'hello:world'], value } + ]); + }); + + [ + 'hello', 'HeLlO' + ].forEach(name => it('emits name in value', () => { + const doc = { type: 'district_hospital', name }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0, name: name.toLowerCase() }); + expect(emitted).to.deep.equal([ + { key: ['district_hospital', name.toLowerCase()], value }, + { key: ['district_hospital', `name:${name.toLowerCase()}`], value } + ]); + })); + + [ + null, undefined, { hello: 'world' }, {}, true + ].forEach(hello => it('emits nothing when value is not a string or number', () => { + const doc = { type: 'district_hospital', hello }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + })); + + it('emits only key:value when value is number', () => { + const doc = { type: 'district_hospital', hello: 1234 }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0 }); + expect(emitted).to.deep.equal([{ key: ['district_hospital', 'hello:1234'], value }]); + }); + + [ + 't', 'to' + ].forEach(hello => it('emits nothing but key:value when value is too short', () => { + const doc = { type: 'district_hospital', hello }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0 }); + expect(emitted).to.deep.equal([{ key: ['district_hospital', `hello:${hello}`], value }]); + })); + + it('emits nothing when value is empty', () => { + const doc = { type: 'district_hospital', hello: '' }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + }); + + [ + '_id', '_rev', 'type', 'refid', 'geolocation', 'Refid' + ].forEach(key => it('emits nothing for a skipped field', () => { + const doc = { type: 'district_hospital', [key]: 'world' }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + })); + + it('emits nothing for fields that end with "_date"', () => { + const doc = { type: 'district_hospital', reported_date: 'world' }; + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + }); + + it('emits value only once', () => { + const doc = { + type: 'district_hospital', + hello: 'world world', + hello1: 'world', + hello3: 'world', + }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0 }); + expect(emitted).to.deep.equal([ + { key: ['district_hospital', 'world'], value }, + { key: ['district_hospital', 'hello:world world'], value }, + { key: ['district_hospital', 'hello1:world'], value }, + { key: ['district_hospital', 'hello3:world'], value } + ]); + }); + + it('emits each word in a string', () => { + const doc = { + type: 'district_hospital', + hello: `the quick\nBrown\tfox`, + }; + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0 }); + expect(emitted).to.deep.equal([ + { key: ['district_hospital', 'the'], value }, + { key: ['district_hospital', 'quick'], value }, + { key: ['district_hospital', 'brown'], value }, + { key: ['district_hospital', 'fox'], value }, + { key: ['district_hospital', 'hello:the quick\nbrown\tfox'], value }, + ]); + }); + + it('emits non-ascii values', () => { + const doc = { type: 'district_hospital', name: 'बुद्ध Élève' }; + + const emitted = mapFn(doc, true); + + const value = expectedValue({ typeIndex: 0, name: 'बुद्ध élève' }); + expect(emitted).to.deep.equal([ + { key: ['district_hospital', 'बुद्ध'], value }, + { key: ['district_hospital', 'élève'], value }, + { key: ['district_hospital', 'name:बुद्ध élève'], value } + ]); + }); + }); }); - - it('indexes non-ascii doc name', () => { - // when - const emitted = map(nonAsciiDoc); - - // then - utils.assertIncludesPair(emitted, ['person', 'बुद्ध']); - utils.assertIncludesPair(emitted, ['person', 'élève']); - }); - - it('does not index words of less than 3 chars', () => { - // when - const emitted = map(doc); - - // then - utils.assertDoesNotIncludePair(emitted, ['person', 'of']); - }); - - it('does not index non-contact docs', () => { - // when - const emitted = map({ type: 'data_record', name: 'do not index me'}); - - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - assert.equal(emitted.length, 0); - }); - - it('returns configurable type', () => { - // when - const emitted = map(configurableHierarchyDoc); - - // then - utils.assertIncludesPair(emitted, ['chp', 'jessie']); - }); - }); diff --git a/webapp/tests/mocha/unit/views/reports_by_freetext.spec.js b/webapp/tests/mocha/unit/views/reports_by_freetext.spec.js index 49cdaa1b0fc..72dc5a9cf84 100644 --- a/webapp/tests/mocha/unit/views/reports_by_freetext.spec.js +++ b/webapp/tests/mocha/unit/views/reports_by_freetext.spec.js @@ -1,174 +1,181 @@ -const _ = require('lodash'); -const assert = require('chai').assert; -const utils = require('./utils'); - -const doc = { - _id: '7383B568-4A6C-2C97-B463-3CC2630A562E', - _rev: '1-ddec60a626c8f5b17b0f5fcdc2031c39', - content: '', - 'fields': { - 'inputs': { - 'source': 'task', - source_id: '82CFA683-D6F5-3427-95C7-45D792EA5A08', - t_lmp_date: '2015-09-23T07:00:00.000Z', - contact: { - _id: 'd57c76c42de8b76bfcc2c07956ce879f', - name: 'Patient With A Problem', - date_of_birth: '1985-03-24', - sex: 'female', - phone: '+254777888999', - parent: { - contact: { - phone: '+254777888999' - } - } - } - }, - patient_age_in_years: '25', - patient_contact_phone: '+254777888999', - patient_id: 'd57c76c42de8b76bfcc2c07956ce879f', - patient_name: 'Patient With A Problem', - lmp_date: '2015-09-23T07:00:00.000Z', - follow_up_method: 'in_person', - delivery_plan_discussed: '', - danger_signs: 'd7', - referral_follow_up_needed: 'true', - days_since_lmp: '271.69', - weeks_since_lmp: '38.81', - edd: 'Jun 29, 2016', - p_note: '', - group_followup_options: { - g_follow_up_method: 'in_person', - call_button: '' - }, - group_danger_signs: { - g_danger_signs: 'd7' - }, - group_review: { - submit: '', - r_summary: '', - r_pregnancy_details: '', - r_referral: '', - r_referral_note: '', - r_danger_sign1: '', - r_danger_sign2: '', - r_danger_sign3: '', - r_danger_sign4: '', - r_danger_sign5: '', - r_danger_sign6: '', - r_danger_sign7: '', - r_danger_sign8: '', - r_danger_sign9: '', - r_reminders: '', - r_reminder_trim1: '', - r_reminder_trim2: '', - r_reminder_trim3: '', - r_followup_instructions: 'Follow up in 1 day to ensure that patient goes to a health facility', - r_followup: '', - r_followup_note: '' - }, - group_delivery_plan: { - no_delivery_plan_discussed: '', - delivery_plan: '', - g_delivery_plan_discussed: '' - }, - group_healthy_newborn_practices: { - healthy_newborn_practices: '', - submit: '' - } - }, - form: 'pregnancy_visit', - type: 'data_record', - content_type: 'xml', - reported_date: 1466466049001, - contact: { - name: 'Robert', - phone: '+254777111222', - parent: { - type: 'health_center', - name: 'HippieLand CHP Area1', - contact: { - type: 'person', - name: 'HippieLand CHP Area1 Person', - phone: '+254702123123' - }, - _id: '6850E77F-5FFC-9B01-8D5B-3D6E33DFA73E', - _rev: '1-9ed31f1ee070eb64351c6f2a4f8dfe5c' - }, - type: 'person', - _id: 'DFEF75F5-4D25-EA47-8706-2B12500EFD8F', - _rev: '1-4c6b5d0545c0aba0b5f9213cc29b4e14' - }, - from: '+254777111222', - hidden_fields: [ - 'days_since_lmp', - 'weeks_since_lmp', - 'p_note', - 'group_followup_options', - 'group_danger_signs', - 'group_review', - 'group_delivery_plan', - 'group_healthy_newborn_practices' - ] +const { loadView, buildViewMapFn } = require('./utils'); +const medicOfflineFreetext = require('../../../../src/js/bootstrapper/offline-ddocs/medic-offline-freetext'); +const { expect } = require('chai'); + +const createReport = (data = {}) => { + return { + type: 'data_record', + form: 'test', + reported_date: 1466466049001, + ...data, + }; }; -describe('reports_by_freetext view', () => { +describe('reports_by_freetext', () => { + [ + ['online view', loadView('medic-db', 'medic-client', 'reports_by_freetext')], + ['offline view', buildViewMapFn(medicOfflineFreetext.views.reports_by_freetext.map)], + ].forEach(([name, mapFn]) => { + describe(name, () => { + afterEach(() => mapFn.reset()); - it('indexes doc name', () => { - // given - const map = utils.loadView('medic-db', 'medic-client', 'reports_by_freetext'); + [ + undefined, + 'invalid', + 'contact', + 'person', + ].forEach(type => it('emits nothing when type is invalid', () => { + const doc = createReport({ type }); + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + })); - // when - const emitted = map(doc); + [ + undefined, + null, + '', + ].forEach(form => it('emits nothing when form is not valued', () => { + const doc = createReport({ form }); + const emitted = mapFn(doc, true); + expect(emitted).to.be.empty; + })); - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - const flattened = _.flattenDeep(emitted); - assert.include(flattened, 'patient'); - assert.include(flattened, 'with'); - assert.include(flattened, 'problem'); - }); + [ + null, undefined, { hello: 'world' }, {}, true + ].forEach(hello => it('emits nothing for a field when value is not a string or number', () => { + const doc = createReport({ hello }); - it('indexes non-ascii doc name', () => { - // given - const map = utils.loadView('medic-db', 'medic-client', 'reports_by_freetext'); + const emitted = mapFn(doc, true); - // when - doc.name = 'बुद्ध Élève'; - const emitted = map(doc); + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + ]); + })); - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - const flattened = _.flattenDeep(emitted); - assert.include(flattened, 'बुद्ध'); - assert.include(flattened, 'élève'); - }); + it('emits only key:value for field when value is number', () => { + const doc = createReport({ hello: 1234 }); - it('does not index words of less than 3 chars', () => { - // given - const map = utils.loadView('medic-db', 'medic-client', 'reports_by_freetext'); + const emitted = mapFn(doc, true); - // when - const emitted = map(doc); + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + { key: ['hello:1234'], value: doc.reported_date } + ]); + }); - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - const flattened = _.flattenDeep(emitted); - assert.notInclude(flattened, 'a'); - }); + [ + 't', 'to' + ].forEach(hello => it('emits nothing but key:value when value is too short', () => { + const doc = createReport({ hello }); - it('does not index non-reports docs', () => { - // given - const map = utils.loadView('medic-db', 'medic-client', 'reports_by_freetext'); + const emitted = mapFn(doc, true); - // when - const emitted = map({ - type: 'person', - name: 'do not index me' - }); + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + { key: [`hello:${hello}`], value: doc.reported_date } + ]); + })); + + it('emits nothing for field when value is empty', () => { + const doc = createReport({ hello: '' }); + + const emitted = mapFn(doc, true); + + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + ]); + }); + + [ + '_id', '_rev', 'refid', 'content', 'Refid', + ].forEach(key => it(`emits nothing for a skipped field: ${key}`, () => { + const doc = createReport({ [key]: 'world' }); + + const emitted = mapFn(doc, true); + + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + ]); + })); + + it('emits nothing for fields that end with "_date"', () => { + const doc = createReport({ birth_date: 'world' }); + + const emitted = mapFn(doc, true); - // then - // Keys are arrays, so flatten the array of arrays for easier asserts. - assert.equal(emitted.length, 0); + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + ]); + }); + + it('emits value only once', () => { + const doc = createReport({ + hello: 'world world', + hello1: 'world', + hello3: 'world', + }); + + const emitted = mapFn(doc, true); + + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + { key: ['world'], value: doc.reported_date }, + { key: ['hello:world world'], value: doc.reported_date }, + { key: ['hello1:world'], value: doc.reported_date }, + { key: ['hello3:world'], value: doc.reported_date } + ]); + }); + + it('normalizes keys and values to lowercase', () => { + const doc = createReport({ HeLlo: 'WoRlD', NBR: 1234 }); + + const emitted = mapFn(doc, true); + + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + { key: ['world'], value: doc.reported_date }, + { key: ['hello:world'], value: doc.reported_date }, + { key: ['nbr:1234'], value: doc.reported_date }, + ]); + }); + + it('emits each word in a string', () => { + const doc = createReport({ hello: `the quick\nBrown\tfox` }); + + const emitted = mapFn(doc, true); + + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + { key: ['the'], value: doc.reported_date }, + { key: ['quick'], value: doc.reported_date }, + { key: ['brown'], value: doc.reported_date }, + { key: ['fox'], value: doc.reported_date }, + { key: ['hello:the quick\nbrown\tfox'], value: doc.reported_date }, + ]); + }); + + it('emits non-ascii values', () => { + const doc = createReport({ name: 'बुद्ध Élève' }); + + const emitted = mapFn(doc, true); + + expect(emitted).to.deep.equal([ + { key: ['test'], value: doc.reported_date }, + { key: ['form:test'], value: doc.reported_date }, + { key: ['बुद्ध'], value: doc.reported_date }, + { key: ['élève'], value: doc.reported_date }, + { key: ['name:बुद्ध élève'], value: doc.reported_date } + ]); + }); + }); }); }); diff --git a/webapp/tests/mocha/unit/views/utils.js b/webapp/tests/mocha/unit/views/utils.js index e827ab349a5..39b0cb1d068 100644 --- a/webapp/tests/mocha/unit/views/utils.js +++ b/webapp/tests/mocha/unit/views/utils.js @@ -5,9 +5,7 @@ const vm = require('vm'); const MAP_ARG_NAME = 'doc'; -module.exports.loadView = (dbName, ddocName, viewName) => { - const mapPath = path.join(__dirname, '../../../../../ddocs', dbName, ddocName, 'views', viewName, '/map.js'); - const mapString = fs.readFileSync(mapPath, 'utf8'); +module.exports.buildViewMapFn = (mapString) => { const mapScript = new vm.Script('(' + mapString + ')(' + MAP_ARG_NAME + ');'); const emitted = []; @@ -35,6 +33,12 @@ module.exports.loadView = (dbName, ddocName, viewName) => { return mapFn; }; +module.exports.loadView = (dbName, ddocName, viewName) => { + const mapPath = path.join(__dirname, '../../../../../ddocs', dbName, ddocName, 'views', viewName, '/map.js'); + const mapString = fs.readFileSync(mapPath, 'utf8'); + return module.exports.buildViewMapFn(mapString); +}; + module.exports.assertIncludesPair = (array, pair) => { assert.ok(array.find((keyArray) => keyArray[0] === pair[0] && keyArray[1] === pair[1])); };