Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-148253 Quiz Entry Coverage #2305

Merged
merged 26 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1b2e32c
Initial quiz-entry block with ml field
colloyd Mar 22, 2024
fdee326
MWPW-144810: Quiz Entry - Add option cards and text to the block (#2095)
JackySun9 Apr 1, 2024
e5f85f7
Quiz entry block (#2103)
JackySun9 Apr 3, 2024
eb9d64a
MWPW-146243 - Quiz entry code optimization (#2121)
colloyd Apr 9, 2024
b536cc2
MWPW-146034 - Quiz entry block accessibility (#2139)
colloyd Apr 11, 2024
07a349f
MWPW-146036 - Rig up quiz entry button (#2190)
colloyd Apr 24, 2024
270c14b
Quiz entry block (#2204)
JackySun9 Apr 26, 2024
9496008
MWPW-144022: Quiz entry block (#2227)
fullcolorcoder Apr 30, 2024
2c3111b
MWPW-147482 - ML Input Bulletproofing (#2242)
colloyd May 1, 2024
779c9a0
MWPW-147683 - CSS Cleanup (#2267)
colloyd May 8, 2024
aff5965
carousel starts
fullcolorcoder Apr 17, 2024
6b3f521
got buttons working
fullcolorcoder Apr 19, 2024
1646ab3
MWPW-144022: Prototype carousel refinement, keybord controls updated
fullcolorcoder Apr 29, 2024
b2990db
linting fixes
fullcolorcoder Apr 29, 2024
f0dd5ec
Tests
fullcolorcoder May 10, 2024
a4bcd7c
working tests update
fullcolorcoder May 10, 2024
3a09e65
linting
fullcolorcoder May 10, 2024
6aee0ea
linting
fullcolorcoder May 10, 2024
18677b3
Update quiz-entry.js with default vals
fullcolorcoder May 10, 2024
5c543a4
Bring back debug
fullcolorcoder May 10, 2024
49b0bc2
debugging debug
fullcolorcoder May 10, 2024
a0b27ca
Improved test coverage
fullcolorcoder May 14, 2024
0e652ce
resolve conflicts
fullcolorcoder May 14, 2024
e9ac496
resolve conflicts
fullcolorcoder May 14, 2024
c1b6e52
restore style
fullcolorcoder May 14, 2024
afd32b1
formatting fix
fullcolorcoder May 14, 2024
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
8 changes: 7 additions & 1 deletion libs/blocks/quiz-entry/quiz-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { mlField, getMLResults } from './mlField.js';
import { GetQuizOption } from './quizoption.js';
import { quizPopover, getSuggestions } from './quizPopover.js';

export const locationWrapper = {
redirect: (url) => {
window.location = url;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered assign? It may be useful to navigate back, if that's desired.

},
};

const App = ({
quizPath,
maxQuestions,
Expand Down Expand Up @@ -222,7 +228,7 @@ const App = ({
if (questionCount.current === maxQuestions || currentQuizState.userFlow.length === 1) {
if (!debug) {
setSelectedQuestion(null);
window.location = quizPath;
locationWrapper.redirect(quizPath);
}
} else {
setSelectedCards({});
Expand Down
7 changes: 0 additions & 7 deletions libs/blocks/quiz-entry/quizoption.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,6 @@ export const GetQuizOption = ({
}
};

useEffect(() => {
fullcolorcoder marked this conversation as resolved.
Show resolved Hide resolved
const entry = document.querySelector('.quiz-entry');
if (entry && entry.querySelector('.no-carousel')) {
entry.removeChild(entry.querySelector('.no-carousel'));
}
}, []);

return html`
<div class="quiz-options-container" role="group" aria-labelledby="question" tabindex="0" onkeydown=${handleKey}>
${index > 0 && html`<button onClick=${prev} class="carousel-arrow arrow-prev ${isRTL ? 'rtl' : ''}"></button>`}
Expand Down
5 changes: 4 additions & 1 deletion libs/blocks/quiz-entry/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ export const handleSelections = (prevSelections, selectedQuestion, selections) =
// de-dup any existing data if they use the ml field and cards.
if (prevSelections.length > 0) {
prevSelections.forEach((selection) => {
if (selection.selectedQuestion === selectedQuestion) {
const jsonSelectionSelectedQustion = JSON.stringify(selection.selectedQuestion);
colloyd marked this conversation as resolved.
Show resolved Hide resolved
const jsonSelectedQuesion = JSON.stringify(selectedQuestion[0].selectedQuestion);
const isSameQuestion = jsonSelectionSelectedQustion === jsonSelectedQuesion;
if (isSameQuestion) {
Comment on lines +81 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can skip caching here and reduce the lines of code.

Suggested change
const jsonSelectionSelectedQustion = JSON.stringify(selection.selectedQuestion);
const jsonSelectedQuesion = JSON.stringify(selectedQuestion[0].selectedQuestion);
const isSameQuestion = jsonSelectionSelectedQustion === jsonSelectedQuesion;
if (isSameQuestion) {
if (JSON.stringify(selection.selectedQuestion) === JSON.stringify(selectedQuestion[0].selectedQuestion)) {

selection.selectedCards = selections;
isNewQuestion = false;
}
Expand Down
4 changes: 2 additions & 2 deletions test/blocks/quiz-entry/mocks/mock-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const resultsMock = {
options: 'photo',
title: 'Photography',
text: 'Edit or organize my photos',
icon: '',
icon: 'https://milo.adobe.com/drafts/quiz/quiz-ai/search.svg',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this ever get loaded? If so, it should be switched to a mock resource

image: 'https://main--milo--adobecom.hlx.page/drafts/colloyd/quiz-entry/images/photography.png',
},
{
Expand Down Expand Up @@ -664,7 +664,7 @@ const resultsMock = {
},
{
options: 'video',
next: 'q-rather,q-video',
next: 'RESET',
type: 'card',
endpoint: '',
'api-key': '',
Expand Down
145 changes: 144 additions & 1 deletion test/blocks/quiz-entry/quiz-entry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import sinon from 'sinon';
import init from '../../../libs/blocks/quiz-entry/quiz-entry.js';
import { getSuggestions } from '../../../libs/blocks/quiz-entry/quizPopover.js'; // Correct the path as needed
import { getSuggestions } from '../../../libs/blocks/quiz-entry/quizPopover.js';

let fetchStub;
let quizEntryElement;
Expand Down Expand Up @@ -156,4 +156,147 @@ describe('Quiz Entry Component', () => {
await new Promise((resolve) => setTimeout(resolve, 100));
expect(continueButton.classList.contains('disabled')).to.be.false;
});
it('should navigate the carousel using keyboard commands', async () => {
const options = document.querySelectorAll('.quiz-option');
const option = document.querySelector('.quiz-option');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this just be options[0]?

option.click();
await new Promise((resolve) => setTimeout(resolve, 100));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of having timeouts in unit tests, as they tend to add up and then the test suite takes a long time to run. I think there was a way around this with clock.runAllAsync() or something of the sort. In total this file adds around 2s of wait time. Multiply that by more such files and our PR checks will take a bit too much time to run.

const carousel = document.querySelector('.quiz-options-container');
const rightArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowRight' });
const leftArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowLeft' });
carousel.dispatchEvent(rightArrowEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
carousel.dispatchEvent(leftArrowEvent);
Comment on lines +165 to +169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Same here, we can skip caching

Suggested change
const rightArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowRight' });
const leftArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowLeft' });
carousel.dispatchEvent(rightArrowEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
carousel.dispatchEvent(leftArrowEvent);
carousel.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
await new Promise((resolve) => setTimeout(resolve, 100));
carousel.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }));

await new Promise((resolve) => setTimeout(resolve, 100));
const leftArrow = document.querySelector('.carousel-arrow.arrow-prev');
expect(leftArrow).to.not.exist;
Comment on lines +171 to +172
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Same here, we can skip caching

Suggested change
const leftArrow = document.querySelector('.carousel-arrow.arrow-prev');
expect(leftArrow).to.not.exist;
expect(document.querySelector('.carousel-arrow.arrow-prev')).to.not.exist;


const tabKeyEvent = new KeyboardEvent('keydown', { key: 'Tab' });
option.dispatchEvent(tabKeyEvent);
Comment on lines +174 to +175
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Same here, we can skip caching. There are many optimising opportunities like this in the file, I won't add a comment for all of them. This is just a possible improvement to the code compactness, nothing critical.

Suggested change
const tabKeyEvent = new KeyboardEvent('keydown', { key: 'Tab' });
option.dispatchEvent(tabKeyEvent);
option.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));

await new Promise((resolve) => setTimeout(resolve, 100));
expect(option.classList.contains('selected')).to.be.true;

const spaceKeyEvent = new KeyboardEvent('keydown', { key: ' ', keyCode: 32 });
carousel.dispatchEvent(spaceKeyEvent);
await new Promise((resolve) => setTimeout(resolve, 100));

const enterKeyEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
});
carousel.dispatchEvent(enterKeyEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(options[1].classList.contains('selected')).to.be.false;
});
});

describe('RTL Quiz Entry', () => {
beforeEach(async () => {
window.lana = { log: sinon.stub() };
fetchStub = sinon.stub(window, 'fetch');
fetchStub.resolves({
ok: true,
json: () => Promise.resolve({ suggested_completions: ['designer desk', 'design logos'] }),
});
document.body.innerHTML = await readFile({ path: './mocks/index.html' });
document.documentElement.setAttribute('dir', 'rtl');
quizEntryElement = document.querySelector('.quiz-entry');
await init(quizEntryElement, quizConfig);
await new Promise((resolve) => setTimeout(resolve, 100));
});

afterEach(() => {
sinon.restore();
});

it('should navigate the carousel using keyboard commands', async () => {
const options = document.querySelectorAll('.quiz-option');
const option = document.querySelector('.quiz-option');
option.click();
await new Promise((resolve) => setTimeout(resolve, 100));
const carousel = document.querySelector('.quiz-options-container');
const rightArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowRight' });
const leftArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowLeft' });
carousel.dispatchEvent(rightArrowEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
carousel.dispatchEvent(leftArrowEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
const leftArrow = document.querySelector('.carousel-arrow.arrow-prev');
expect(leftArrow).to.exist;

const tabKeyEvent = new KeyboardEvent('keydown', { key: 'Tab' });
option.dispatchEvent(tabKeyEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(option.classList.contains('selected')).to.be.false;

const spaceKeyEvent = new KeyboardEvent('keydown', { key: ' ', keyCode: 32 });
carousel.dispatchEvent(spaceKeyEvent);
await new Promise((resolve) => setTimeout(resolve, 100));

const enterKeyEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
});
carousel.dispatchEvent(enterKeyEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(options[1].classList.contains('selected')).to.be.false;
});
});

describe('ML Result Trigger', () => {
beforeEach(async () => {
window.lana = { log: sinon.stub() };
fetchStub = sinon.stub(window, 'fetch');
const mockApiResponse = {
statusCode: 200,
data: {
data: [
{
ficode: 'illustrator_cc',
prob: '0.33',
},
{
ficode: 'indesign_cc',
prob: '0.27',
},
{
ficode: 'free_spark',
prob: '0.22',
},
],
jobName: '',
},
};
fetchStub.resolves({
ok: true,
json: () => Promise.resolve(mockApiResponse.data),
});
document.body.innerHTML = await readFile({ path: './mocks/index.html' });
quizEntryElement = document.querySelector('.quiz-entry');
await init(quizEntryElement, quizConfig);
await new Promise((resolve) => setTimeout(resolve, 100));
});

afterEach(() => {
sinon.restore();
});

it('Should trigger results fetching scenario', async () => {
const mlInputField = document.querySelector('#quiz-input');
const testInput = 'design';
const inputEvent = new Event('input', { bubbles: true });
mlInputField.value = testInput;
mlInputField.dispatchEvent(inputEvent);
await new Promise((resolve) => setTimeout(resolve, 100));

const enterKeyEvent = new KeyboardEvent('keypress', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
});
mlInputField.dispatchEvent(enterKeyEvent);
expect(mlInputField.value).to.equal('design');
});
});
149 changes: 149 additions & 0 deletions test/blocks/quiz-entry/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* eslint-disable no-promise-executor-return */
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import sinon from 'sinon';
import { handleNext, getQuizJson, handleSelections, getQuizEntryData } from '../../../libs/blocks/quiz-entry/utils.js'; // Correct the path as needed

let fetchStub;
const { default: mockData } = await import('./mocks/mock-data.js');
const mockQuestionsData = mockData.questions;
const mockStringsData = mockData.strings;
const quizConfig = {
quizPath: '/drafts/quiz/',
maxQuestions: 1,
analyticsQuiz: 'uarv4',
analyticsType: 'cc:app-reco',
questionData: undefined,
stringsData: undefined,
};
const selectedQuestion = {
questions: 'q-category',
'max-selections': '3',
'min-selections': '1',
};
const userInputSelections = { photo: true };
const userInputSelectionsNot = { '3d': true };
const userInputSelectionsReset = { video: true };

const userFlow = [];
const nextFlow = { nextFlow: ['q-rather', 'q-photo'] };
const nextFlowNot = { nextFlow: ['q-3d'] };
const nextFlowReset = { nextFlow: [] };
const prevSelections = [];
const selections = ['photo'];
const nextSelectionsExpected = {
nextSelections: [
{
selectedCards: [
'photo',
],
selectedQuestion: {
'max-selections': '3',
'min-selections': '1',
questions: 'q-category',
},
},
],
};

describe('Quiz Entry Utils', () => {
beforeEach(async () => {
window.lana = { log: sinon.stub() };
fetchStub = sinon.stub(window, 'fetch');
fetchStub.resolves({
ok: true,
json: () => Promise.resolve(mockData),
});
});

afterEach(() => {
sinon.restore();
});

it('should handle the next flow of questions', async () => {
const nextQuestion = handleNext(
mockQuestionsData,
selectedQuestion,
userInputSelections,
userFlow,
);
expect(nextQuestion).to.deep.equal(nextFlow);
});

it('should handle the next flow of questions with not()', async () => {
const nextQuestion = handleNext(
mockQuestionsData,
selectedQuestion,
userInputSelectionsNot,
userFlow,
);
expect(nextQuestion).to.deep.equal(nextFlowNot);
});

it('should handle the next flow of questions with reset()', async () => {
const nextQuestion = handleNext(
mockQuestionsData,
selectedQuestion,
userInputSelectionsReset,
userFlow,
);
expect(nextQuestion).to.deep.equal(nextFlowReset);
});

it('should fetch quiz data', async () => {
const [questions, strings] = await getQuizJson('./mocks/');
expect(questions.questions).to.deep.equal(mockQuestionsData);
expect(strings.strings).to.deep.equal(mockStringsData);
});
});

describe('Quiz Entry Utils failed request', () => {
beforeEach(async () => {
window.lana = { log: sinon.stub() };
fetchStub = sinon.stub(window, 'fetch');
fetchStub.resolves({ ok: false });
});

afterEach(() => {
sinon.restore();
});

it('should log an error when fetching quiz data fails', async () => {
await getQuizJson('./mocks/');
expect(window.lana.log.called).to.be.true;
});
it('should return nextSelections on handleSelections', async () => {
const nextSelections = handleSelections(prevSelections, selectedQuestion, selections);
expect(nextSelections).to.deep.equal(nextSelectionsExpected);
});

it('should de-dup any existing data if they use the ml field and cards.', async () => {
const prevSelectionsLength = [{
selectedQuestion: {
'max-selections': '3',
'min-selections': '1',
questions: 'q-category',
},
}];

const selectedQuestionPrev = [{
selectedQuestion: {
'max-selections': '3',
'min-selections': '1',
questions: 'q-category',
},
}];

const nextSelections = handleSelections(prevSelectionsLength, selectedQuestionPrev, selections);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(nextSelections).to.deep.equal(nextSelections);
});

it('should return quizPath, maxQuestions, analyticsQuiz, analyticsType, questionData', async () => {
document.body.innerHTML = await readFile({ path: './mocks/index.html' });
const el = document.querySelector('.quiz-entry');

const quizEntryData = await getQuizEntryData(el);
expect(quizEntryData).to.deep.equal(quizConfig);
});
});
Loading