Skip to content
This repository has been archived by the owner on Apr 27, 2023. It is now read-only.

Do not submit survey values that are not defined #636

Merged
merged 7 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[Full changelog](https://github.com/mozilla-rally/core-addon/compare/v1.3.1...master)

* [#624](https://github.com/mozilla-rally/rally-core-addon/pull/624): Update the `income` field to use the key `exactIncome`; update `metrics.yaml` to change `income` to `exact_income` of type `quantity`.
* [#636](https://github.com/mozilla-rally/rally-core-addon/pull/636): Remove undefined values from demographic survey submission.

# v1.3.1 (2021-05-20)

Expand Down
3 changes: 1 addition & 2 deletions src/routes/Onboarding.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
dispatch("onboarding-complete");
}

let results;
let formattedResults;
</script>

Expand All @@ -65,7 +64,7 @@
{:else if view === 'terms'}
<TermsOfService />
{:else if view === 'demographics'}
<Demographics bind:results bind:formattedResults />
<Demographics bind:formattedResults />
{/if}
</Main>
<OnboardingCTAContainer
Expand Down
34 changes: 18 additions & 16 deletions src/routes/demographics/Content.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import { formatInput, formatAnswersForResponse } from "./formatters";

export let results = createResultObject(schema);
// create the outputted formatted results.
export let formattedResults = formatAnswersForResponse(schema, results, inputFormatters);
$: formattedResults = formatAnswersForResponse(schema, results, inputFormatters);
export let workingResults = createResultObject(schema, results);
$: workingResults = createResultObject(schema, workingResults);
// create the outputted formatted workingResults.
export let formattedResults = formatAnswersForResponse(schema, workingResults, inputFormatters);
$: formattedResults = formatAnswersForResponse(schema, workingResults, inputFormatters);
</script>

<style>
Expand Down Expand Up @@ -141,17 +143,17 @@
<hr />

<form class="mzp-c-form">
{#each Object.keys(results) as question}
{#each Object.keys(workingResults) as question}
<fieldset class="mzp-c-field-set" class:mzp-c-field-set-text={schema[question].type === 'text'}>
<legend class="mzp-c-field-label"
for={schema[question].key}
class:remove-bottom-margin={schema[question].sublabel}
>
{schema[question].label}
{#if questionIsAnswered(results[question], schema[question].type)}
{#if questionIsAnswered(workingResults[question], schema[question].type)}
<ClearAnswerButton on:click={(e) => {
e.preventDefault();
results[question] = clearAnswer(schema[question].type);
workingResults[question] = clearAnswer(schema[question].type);
}} />
{/if}
</legend>
Expand All @@ -171,27 +173,27 @@
class:mzp-is-error={
inputFormatters.showErrors(question) &&
inputFormatters.hasValidator(question) &&
inputFormatters[question].isInvalid(results[question])
inputFormatters[question].isInvalid(workingResults[question])
}>
<input
type="text"
use:formatInput={inputFormatters[question]}
class:right={inputFormatters[question].alignRight}
on:blur={(event) => { results[question] = event.target.value; }}
on:focus={(event) => { results[question] = event.target.value; }}
on:input={(event) => { results[question] = event.target.value; }}
value={results[question]}
on:blur={(event) => { workingResults[question] = event.target.value; }}
on:focus={(event) => { workingResults[question] = event.target.value; }}
on:input={(event) => { workingResults[question] = event.target.value; }}
value={workingResults[question]}
/>
<span style="min-height: 24px; display: block;">
{#if
inputFormatters.showErrors(question) && // show errors if blurred
inputFormatters.hasValidator(question) && // show errors if there's a validator for this question
inputFormatters[question].isInvalid(results[question])
inputFormatters[question].isInvalid(workingResults[question])
}
<span
class="mzp-c-fieldnote"
transition:fly={{ duration: 300, y: 5 }}>
{inputFormatters[question].isInvalid(results[question])}
{inputFormatters[question].isInvalid(workingResults[question])}
</span>
{/if}
</span>
Expand All @@ -204,14 +206,14 @@
class="mzp-c-choice-control"
type="radio"
id="answer-{answer.key}"
bind:group={results[question]}
bind:group={workingResults[question]}
value={answer.key} />
{:else if schema[question].type === 'multi'}
<input
class="mzp-c-choice-control"
type="checkbox"
id="answer-{answer.key}"
bind:group={results[question]}
bind:group={workingResults[question]}
value={answer.key} />
{/if}
<label class="mzp-c-choice-label" for="answer-{answer.key}">
Expand All @@ -225,5 +227,5 @@
{/each}
</form>
<!-- Add a slot to aid in -->
<slot name='call-to-action' formattedResults={formatAnswersForResponse(schema, results, inputFormatters)} validated={inputFormatters.validateAllQuestions(schema, results)}></slot>
<slot name='call-to-action' formattedResults={formatAnswersForResponse(schema, workingResults, inputFormatters)} validated={inputFormatters.validateAllQuestions(schema, workingResults)}></slot>
</div>
22 changes: 15 additions & 7 deletions src/routes/demographics/formatters.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { questionIsAnswered } from "./survey-tools.js";

/** This Svelte action will format an input element on input, focus, and blur events.
* It takes in a formatter below of type FieldFormatter.
*/
Expand Down Expand Up @@ -93,6 +93,9 @@ export class FieldFormatter {

export function currencyFormatter() {
const formatCurrencyInput = (value) => {
if (value === undefined) {
return '';
}
const r = /^\d/;
return value.split('').filter(a => r.test(a)).join('');
}
Expand Down Expand Up @@ -138,6 +141,9 @@ export class FieldFormatter {

export function zipcodeFormatter() {
const formatZipcodeInput = (value) => {
if (value === undefined) {
return '';
}
const r = /^\d/;
return value.split('').filter(a => r.test(a)).join('').slice(0,5);
}
Expand All @@ -148,6 +154,7 @@ export class FieldFormatter {
zipcode.formatForEditing(formatZipcodeInput);
zipcode.formatForResponse(formatZipcodeInput);
zipcode.setValidator((text) => {
if (text === undefined) return false;
const number = Number(text);
if (text.length > 0 && text.length < 5) return 'zipcode must be a five-digit number';
if (Number.isNaN(number)) return 'zipcode must be a five-digit number';
Expand Down Expand Up @@ -214,19 +221,20 @@ export function createInputFormatters(schema) {
}
}

export function _formatFor(schema, answers, formatters, formatType) {
export function _formatFor(schema, answers, formatters, formatType, removeUnanswered = false) {
const transformedAnswers = {};
Object.keys(schema).forEach(key => {
const answer = answers[key];
// currently, only text fields can have response transformations.

if (!removeUnanswered || questionIsAnswered(answer, schema[key].type))
if (schema[key].type === 'text') {
if (formatters.has(key, formatType)) {
transformedAnswers[key] = formatters[key][formatType](answer);
} else {
transformedAnswers[key] = answer;
}
} else if (schema[key].type === 'multi') {
transformedAnswers[key] = [...answer];
transformedAnswers[key] = answer ? [...answer] : [];
} else if (schema[key].type === 'single') {
transformedAnswers[key] = answer;
}
Expand All @@ -235,7 +243,7 @@ export function _formatFor(schema, answers, formatters, formatType) {
}

export function formatAnswersForResponse(schema, answers, formatters) {
return _formatFor(schema, answers, formatters, 'response');
return _formatFor(schema, answers, formatters, 'response', true);
}

export function formatAnswersForDisplay(schema, answers, formatters) {
Expand Down
25 changes: 23 additions & 2 deletions src/routes/demographics/survey-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

export function createResultObject(schema) {
/**
* Constructs an answer object based on the schema & any pre-existing answers.
* @param {object} schema
* @param {object} currentAnswers the current survey answers, if any. If there is an entry in currentAnswers
* that does not exist in the schema, will ignore the value and only impute values outlined in the schema.
* This ensures that the working answer object will always be up-to-date with the latest schema.
* @returns {object} the schema with default answers to questions, and any additional
* pre-existing answers from currentAnswers.
*/
export function createResultObject(schema, currentAnswers) {
return Object.values(schema).reduce((acc, config) => {
let defaultValue = undefined;
switch (config.type) {
Expand All @@ -22,18 +31,30 @@ export function createResultObject(schema) {
break;
}
// create response entry here.
acc[config.key] = defaultValue;
let answer;
if (currentAnswers && currentAnswers[config.key] !== undefined) {
answer = currentAnswers[config.key];
} else {
answer = defaultValue;
}
acc[config.key] = answer;
return acc;
}, {});
}

export function questionIsAnswered(answer, questionType) {
if (answer === undefined) return false;
if (questionType === 'text') return answer.length > 0;
if (questionType === 'single') return answer !== undefined;
if (questionType === 'multi') return answer.length > 0;
throw Error('unknown question type');
}

/**
* Clears the display version of the answer.
* @param {string} questionType the question type (one of text, single, or multi)
* @returns {string || undefined || []}
*/
export function clearAnswer(questionType) {
switch (questionType) {
case "text": {
Expand Down
11 changes: 11 additions & 0 deletions tests/options-page/unit/formatters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,17 @@
// We get RangeError: maximumFractionDigits value is out of range.
assert.equal(formatter.display("90210"), "90210");
assert.equal(formatter.display("90210abcd"), "90210");
assert.equal(formatter.display(undefined), "");
})
it("sets the response value to be a numeric string", function() {
assert.equal(formatter.response("90210"), "90210");
assert.equal(formatter.response("90210abcd!!!!"), "90210");
assert.equal(formatter.response(undefined), "");
})
it("properly validates a zipcode string", function() {
assert.equal(formatter.isInvalid("90210"), false);
assert.equal(formatter.isInvalid("90"), 'zipcode must be a five-digit number');
assert.equal(formatter.isInvalid(undefined), false);
})
})
describe("_formatFor", function() {
Expand Down Expand Up @@ -157,5 +160,13 @@
otherMulti: ["1", "3"]
})
});

it("formats a typical payload for response, removing unanswered questions", function() {
const answers = { income: undefined, zipcode: "90210", other: undefined, otherMulti: []};
const formattedAnswers = _formatFor(schema, answers, formatters, "response", true);
assert.deepEqual(formattedAnswers, {
zipcode: "90210",
})
});
})
})
65 changes: 65 additions & 0 deletions tests/options-page/unit/survey-tools.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { createResultObject } from '../../../src/routes/demographics/survey-tools.js';
import { strict as assert } from 'assert';

const schema = {
income: {
key: "income",
type: "text",
formatter: 'currency'
},
zipcode: {
key: "zipcode",
type: "text",
formatter: "zipcode"
},
other: {
key: "other",
type: "single",
values: [
{"key": 'a', label: "A"},
{"key": 'b', label: "B"},
]
},
otherMulti: {
key: "otherMulti",
type: "multi",
values: [
{key: "1", label: "One"},
{key: "2", label: "Two"},
{key: "3", label: "Three"},
]
}
};

describe('survey-tools', function() {
describe('creatResultObject', function() {
it("creates an empty resultObject based on a schema", function() {
assert.deepEqual(createResultObject(schema), {
income: '',
zipcode: '',
other: undefined,
otherMulti: []
})
});
it("creates a new resultObject based on a schema and additional answers", function() {
assert.deepEqual(createResultObject(schema, {income: '10000', otherMulti: ["1", "3"]}), {
income: '10000',
zipcode: '',
other: undefined,
otherMulti: ["1", "3"]
})
})
it("ignores any extraneous pre-existing answers passed in and only constructs based on the schema keys", function() {
assert.deepEqual(createResultObject(schema, {income: '10000', otherMulti: ["1", "3"], whateverValue: "test value that won't appear"}), {
income: '10000',
zipcode: '',
other: undefined,
otherMulti: ["1", "3"]
})
})
})
})