mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 10:22:16 +00:00
feat(client,challenge-parser): support audio and transcript in quiz questions (#65711)
This commit is contained in:
@@ -351,10 +351,27 @@ type Quiz {
|
||||
questions: [QuizQuestion]
|
||||
}
|
||||
|
||||
type QuizAudio {
|
||||
filename: String
|
||||
startTimestamp: Float
|
||||
finishTimestamp: Float
|
||||
}
|
||||
|
||||
type QuizTranscriptLine {
|
||||
character: String
|
||||
text: String
|
||||
}
|
||||
|
||||
type QuizAudioData {
|
||||
audio: QuizAudio
|
||||
transcript: [QuizTranscriptLine]
|
||||
}
|
||||
|
||||
type QuizQuestion {
|
||||
text: String
|
||||
distractors: [String]
|
||||
answer: String
|
||||
audioData: QuizAudioData
|
||||
}
|
||||
|
||||
type RequiredResource {
|
||||
|
||||
@@ -347,10 +347,27 @@ type Quiz = {
|
||||
questions: QuizQuestion[];
|
||||
};
|
||||
|
||||
type QuizAudio = {
|
||||
filename: string;
|
||||
startTimestamp?: number | null;
|
||||
finishTimestamp?: number | null;
|
||||
};
|
||||
|
||||
type QuizTranscriptLine = {
|
||||
character: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type QuizAudioData = {
|
||||
audio: QuizAudio;
|
||||
transcript: QuizTranscriptLine[];
|
||||
};
|
||||
|
||||
type QuizQuestion = {
|
||||
text: string;
|
||||
distractors: string[];
|
||||
answer: string;
|
||||
audioData?: QuizAudioData | null;
|
||||
};
|
||||
|
||||
export type CertificateNode = {
|
||||
|
||||
@@ -25,3 +25,9 @@
|
||||
.quiz-answer-label:has(ruby) {
|
||||
line-height: 1.7rem;
|
||||
}
|
||||
|
||||
/* Override global button:hover for the play/pause button in the Quiz component */
|
||||
.quiz-challenge-container button:has(svg.svg-inline--fa):hover {
|
||||
background-color: unset;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
@@ -166,16 +166,36 @@ const ShowQuiz = ({
|
||||
value: 4
|
||||
};
|
||||
|
||||
return {
|
||||
const allAnswers = shuffleArray([...distractors, answer]);
|
||||
|
||||
const audioData = question.audioData?.audio?.filename
|
||||
? {
|
||||
audioUrl: `https://cdn.freecodecamp.org/curriculum/english/animation-assets/sounds/${question.audioData.audio.filename}`,
|
||||
audioStartTime:
|
||||
question.audioData.audio.startTimestamp ?? undefined,
|
||||
audioFinishTime:
|
||||
question.audioData.audio.finishTimestamp ?? undefined,
|
||||
transcript: question.audioData.transcript.length
|
||||
? question.audioData.transcript
|
||||
.map(line => `<p><b>${line.character}</b>: ${line.text}</p>`)
|
||||
.join('')
|
||||
: undefined
|
||||
}
|
||||
: {};
|
||||
|
||||
const questionData = {
|
||||
question: (
|
||||
<PrismFormatted
|
||||
className='quiz-question-label'
|
||||
text={question.text}
|
||||
/>
|
||||
),
|
||||
answers: shuffleArray([...distractors, answer]),
|
||||
correctAnswer: answer.value
|
||||
answers: allAnswers,
|
||||
correctAnswer: answer.value,
|
||||
...audioData
|
||||
};
|
||||
|
||||
return questionData;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -400,6 +420,17 @@ export const query = graphql`
|
||||
distractors
|
||||
text
|
||||
answer
|
||||
audioData {
|
||||
audio {
|
||||
filename
|
||||
startTimestamp
|
||||
finishTimestamp
|
||||
}
|
||||
transcript {
|
||||
character
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tests {
|
||||
|
||||
+19
-1
@@ -44,7 +44,25 @@ Which of the following is NOT a greeting?
|
||||
|
||||
#### --text--
|
||||
|
||||
Which is the correct reply to this sentence: `Hello! You're the new graphic designer, right?`
|
||||
Listen to the audio and select the most appropriate answer.
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "1.1-1.mp3",
|
||||
"startTimestamp": 0,
|
||||
"finishTimestamp": 4.2
|
||||
},
|
||||
"transcript": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"text": "Hello. You're the new graphic designer, right? I'm Maria, the team lead."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"distractors": ["<p>Wrong 1</p>", "<p>Wrong 2</p>", "<p>Wrong 3</p>"],
|
||||
"text": "<p>What does the audio say?</p>",
|
||||
"answer": "<p>Correct answer</p>",
|
||||
"audioData": {
|
||||
"audio": {
|
||||
"filename": "test-audio.mp3",
|
||||
"startTimestamp": 0,
|
||||
"finishTimestamp": 2
|
||||
},
|
||||
"transcript": [
|
||||
{
|
||||
"character": "Speaker",
|
||||
"text": "Hello world"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -289,3 +289,46 @@ test.describe('Quiz challenge', () => {
|
||||
await page.close({ runBeforeUnload: true });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Quiz with audio question', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const fixturePath = path.join(
|
||||
__dirname,
|
||||
'fixtures',
|
||||
'quiz-audio-fixture.json'
|
||||
);
|
||||
const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8')) as Quiz[];
|
||||
|
||||
// Intercept the exact page-data.json for the quiz and inject the fixture
|
||||
await page.route(`**/page-data${quizPath}/page-data.json`, async route => {
|
||||
const response = await route.fetch();
|
||||
const body = await response.text();
|
||||
|
||||
const pageData = JSON.parse(body) as PageData;
|
||||
pageData.result.data.challengeNode.challenge.quizzes = fixture;
|
||||
|
||||
await route.fulfill({
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(pageData)
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(quizPath);
|
||||
});
|
||||
|
||||
test('renders audio player and transcript when question has audio', async ({
|
||||
page
|
||||
}) => {
|
||||
await expect(page.getByRole('radiogroup')).toHaveCount(1);
|
||||
|
||||
const audio = page.locator('audio');
|
||||
await expect(audio).toHaveCount(1);
|
||||
await expect(audio).toHaveAttribute(
|
||||
'src',
|
||||
'https://cdn.freecodecamp.org/curriculum/english/animation-assets/sounds/test-audio.mp3'
|
||||
);
|
||||
|
||||
await page.getByText(/transcript/i).click();
|
||||
await expect(page.getByText('Speaker: Hello world')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Question with audio but empty transcript
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "audio-with-empty-transcript.mp3"
|
||||
},
|
||||
"transcript": []
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Distractor 1
|
||||
|
||||
---
|
||||
|
||||
Distractor 2
|
||||
|
||||
---
|
||||
|
||||
Distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Answer
|
||||
@@ -0,0 +1,42 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Question with invalid JSON audio
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "audio.mp3"
|
||||
},
|
||||
"transcript": [
|
||||
{
|
||||
"character": "A",
|
||||
"text": "Hello"
|
||||
}
|
||||
]
|
||||
// missing comma
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Distractor 1
|
||||
|
||||
---
|
||||
|
||||
Distractor 2
|
||||
|
||||
---
|
||||
|
||||
Distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Answer
|
||||
@@ -0,0 +1,39 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Question with audio missing filename
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {},
|
||||
"transcript": [
|
||||
{
|
||||
"character": "A",
|
||||
"text": "Hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Distractor 1
|
||||
|
||||
---
|
||||
|
||||
Distractor 2
|
||||
|
||||
---
|
||||
|
||||
Distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Answer
|
||||
@@ -0,0 +1,35 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Question with audio but no transcript
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "audio-without-transcript.mp3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Distractor 1
|
||||
|
||||
---
|
||||
|
||||
Distractor 2
|
||||
|
||||
---
|
||||
|
||||
Distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Answer
|
||||
@@ -0,0 +1,29 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Question with audio not in JSON code block
|
||||
|
||||
#### --audio--
|
||||
|
||||
Some plain text audio data
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Distractor 1
|
||||
|
||||
---
|
||||
|
||||
Distractor 2
|
||||
|
||||
---
|
||||
|
||||
Distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Answer
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Question with audio transcript line missing character
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "audio.mp3"
|
||||
},
|
||||
"transcript": [
|
||||
{
|
||||
"text": "Hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Distractor 1
|
||||
|
||||
---
|
||||
|
||||
Distractor 2
|
||||
|
||||
---
|
||||
|
||||
Distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Answer
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Question with audio transcript line missing text
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "audio.mp3"
|
||||
},
|
||||
"transcript": [
|
||||
{
|
||||
"character": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Distractor 1
|
||||
|
||||
---
|
||||
|
||||
Distractor 2
|
||||
|
||||
---
|
||||
|
||||
Distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Answer
|
||||
@@ -0,0 +1,36 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Question with audio transcript not array
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "audio.mp3"
|
||||
},
|
||||
"transcript": "not an array"
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Distractor 1
|
||||
|
||||
---
|
||||
|
||||
Distractor 2
|
||||
|
||||
---
|
||||
|
||||
Distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Answer
|
||||
@@ -0,0 +1,107 @@
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Quiz 1, question 1 with audio timestamps
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "audio-with-timestamps.mp3",
|
||||
"startTimestamp": 1.5,
|
||||
"finishTimestamp": 3.8
|
||||
},
|
||||
"transcript": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"text": "Hello, how are you?"
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"text": "I'm doing well, thank you."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Quiz 1, question 1, distractor 1
|
||||
|
||||
---
|
||||
|
||||
Quiz 1, question 1, distractor 2
|
||||
|
||||
---
|
||||
|
||||
Quiz 1, question 1, distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Quiz 1, question 1, answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Quiz 1, question 2 with audio but no timestamps
|
||||
|
||||
#### --audio--
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": {
|
||||
"filename": "audio-full-file.mp3"
|
||||
},
|
||||
"transcript": [
|
||||
{
|
||||
"character": "Speaker",
|
||||
"text": "This is the full audio transcript."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Quiz 1, question 2, distractor 1
|
||||
|
||||
---
|
||||
|
||||
Quiz 1, question 2, distractor 2
|
||||
|
||||
---
|
||||
|
||||
Quiz 1, question 2, distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Quiz 1, question 2, answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Quiz 1, question 3 without audio
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Quiz 1, question 3, distractor 1
|
||||
|
||||
---
|
||||
|
||||
Quiz 1, question 3, distractor 2
|
||||
|
||||
---
|
||||
|
||||
Quiz 1, question 3, distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Quiz 1, question 3, answer
|
||||
@@ -21,7 +21,7 @@ function plugin() {
|
||||
});
|
||||
}
|
||||
|
||||
function getQuestion(textNodes, distractorNodes, answerNodes) {
|
||||
function getQuestion(textNodes, distractorNodes, answerNodes, audioNodes) {
|
||||
const text = toHtml(textNodes);
|
||||
const distractors = getDistractors(distractorNodes);
|
||||
const answer = toHtml(answerNodes);
|
||||
@@ -31,7 +31,52 @@ function plugin() {
|
||||
throw Error('--distractors-- are missing from quiz question');
|
||||
if (!answer) throw Error('--answer-- is missing from quiz question');
|
||||
|
||||
return { text, distractors, answer };
|
||||
const questionData = { text, distractors, answer };
|
||||
|
||||
// Extract audio data if present
|
||||
if (audioNodes.length > 0) {
|
||||
// Audio should be in a JSON code block
|
||||
if (audioNodes[0].type !== 'code' || audioNodes[0].lang !== 'json') {
|
||||
throw Error('--audio-- section must contain a ```json code block');
|
||||
}
|
||||
|
||||
try {
|
||||
const audioData = JSON.parse(audioNodes[0].value);
|
||||
|
||||
// Validate structure
|
||||
if (!audioData.audio || !audioData.audio.filename) {
|
||||
throw Error('--audio-- section must contain audio.filename');
|
||||
}
|
||||
|
||||
if (!audioData.transcript || !Array.isArray(audioData.transcript)) {
|
||||
throw Error(
|
||||
'--audio-- section must contain transcript as an array'
|
||||
);
|
||||
}
|
||||
|
||||
if (audioData.transcript.length === 0) {
|
||||
throw Error('--audio-- section transcript array cannot be empty');
|
||||
}
|
||||
|
||||
// Validate each transcript line
|
||||
audioData.transcript.forEach((line, index) => {
|
||||
if (!line.character || !line.text) {
|
||||
throw Error(
|
||||
`--audio-- transcript line ${index} must have character and text properties`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
questionData.audioData = audioData;
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw Error('--audio-- section must contain valid JSON');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return questionData;
|
||||
}
|
||||
|
||||
if (quizzesNodes.length > 0) {
|
||||
@@ -60,9 +105,10 @@ function plugin() {
|
||||
const textNodes = getSection(questionTree, '--text--');
|
||||
const distractorNodes = getSection(questionTree, '--distractors--');
|
||||
const answerNodes = getSection(questionTree, '--answer--');
|
||||
const audioNodes = getSection(questionTree, '--audio--');
|
||||
|
||||
quizQuestions.push(
|
||||
getQuestion(textNodes, distractorNodes, answerNodes)
|
||||
getQuestion(textNodes, distractorNodes, answerNodes, audioNodes)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,12 +5,46 @@ import addQuizzes from './add-quizzes';
|
||||
describe('add-quizzes plugin', () => {
|
||||
let mockQuizzesAST;
|
||||
let chineseQuizzesAST;
|
||||
let quizzesWithAudioAST;
|
||||
let quizzesWithAudioInvalidJsonAST;
|
||||
let quizzesWithAudioMissingFilenameAST;
|
||||
let quizzesWithAudioTranscriptNotArrayAST;
|
||||
let quizzesWithAudioEmptyTranscriptAST;
|
||||
let quizzesWithAudioMissingTranscriptAST;
|
||||
let quizzesWithAudioTranscriptMissingCharacterAST;
|
||||
let quizzesWithAudioTranscriptMissingTextAST;
|
||||
let quizzesWithAudioNotJsonCodeBlockAST;
|
||||
const plugin = addQuizzes();
|
||||
let file = { data: {} };
|
||||
|
||||
beforeAll(async () => {
|
||||
mockQuizzesAST = await parseFixture('with-quizzes.md');
|
||||
chineseQuizzesAST = await parseFixture('with-chinese-quizzes.md');
|
||||
quizzesWithAudioAST = await parseFixture('with-quizzes-audio.md');
|
||||
quizzesWithAudioInvalidJsonAST = await parseFixture(
|
||||
'with-quizzes-audio-invalid-json.md'
|
||||
);
|
||||
quizzesWithAudioMissingFilenameAST = await parseFixture(
|
||||
'with-quizzes-audio-missing-filename.md'
|
||||
);
|
||||
quizzesWithAudioTranscriptNotArrayAST = await parseFixture(
|
||||
'with-quizzes-audio-transcript-not-array.md'
|
||||
);
|
||||
quizzesWithAudioEmptyTranscriptAST = await parseFixture(
|
||||
'with-quizzes-audio-empty-transcript.md'
|
||||
);
|
||||
quizzesWithAudioMissingTranscriptAST = await parseFixture(
|
||||
'with-quizzes-audio-missing-transcript.md'
|
||||
);
|
||||
quizzesWithAudioTranscriptMissingCharacterAST = await parseFixture(
|
||||
'with-quizzes-audio-transcript-missing-character.md'
|
||||
);
|
||||
quizzesWithAudioTranscriptMissingTextAST = await parseFixture(
|
||||
'with-quizzes-audio-transcript-missing-text.md'
|
||||
);
|
||||
quizzesWithAudioNotJsonCodeBlockAST = await parseFixture(
|
||||
'with-quizzes-audio-not-json-code-block.md'
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -135,4 +169,111 @@ describe('add-quizzes plugin', () => {
|
||||
'<p>Quiz 1, question 3, answer with <span class="highlighted-text">zhōng wén</span></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse audio sections in quiz questions', () => {
|
||||
plugin(quizzesWithAudioAST, file);
|
||||
const quizzes = file.data.quizzes;
|
||||
|
||||
expect(Array.isArray(quizzes)).toBe(true);
|
||||
expect(quizzes.length).toBe(1);
|
||||
|
||||
const firstQuiz = quizzes[0];
|
||||
const firstQuestion = firstQuiz.questions[0];
|
||||
const secondQuestion = firstQuiz.questions[1];
|
||||
const thirdQuestion = firstQuiz.questions[2];
|
||||
|
||||
// First question has audio with timestamps
|
||||
expect(firstQuestion).toHaveProperty('audioData');
|
||||
expect(firstQuestion.audioData.audio.filename).toBe(
|
||||
'audio-with-timestamps.mp3'
|
||||
);
|
||||
expect(firstQuestion.audioData.audio.startTimestamp).toBe(1.5);
|
||||
expect(firstQuestion.audioData.audio.finishTimestamp).toBe(3.8);
|
||||
expect(firstQuestion.audioData.transcript).toEqual([
|
||||
{
|
||||
character: 'Maria',
|
||||
text: 'Hello, how are you?'
|
||||
},
|
||||
{
|
||||
character: 'Tom',
|
||||
text: "I'm doing well, thank you."
|
||||
}
|
||||
]);
|
||||
expect(firstQuestion.text).toBe(
|
||||
'<p>Quiz 1, question 1 with audio timestamps</p>'
|
||||
);
|
||||
expect(firstQuestion.distractors.length).toBe(3);
|
||||
expect(firstQuestion.answer).toBe('<p>Quiz 1, question 1, answer</p>');
|
||||
|
||||
// Second question has audio without timestamps
|
||||
expect(secondQuestion).toHaveProperty('audioData');
|
||||
expect(secondQuestion.audioData.audio.filename).toBe('audio-full-file.mp3');
|
||||
expect(secondQuestion.audioData.audio.startTimestamp).toBeUndefined();
|
||||
expect(secondQuestion.audioData.audio.finishTimestamp).toBeUndefined();
|
||||
expect(secondQuestion.audioData.transcript).toEqual([
|
||||
{
|
||||
character: 'Speaker',
|
||||
text: 'This is the full audio transcript.'
|
||||
}
|
||||
]);
|
||||
expect(secondQuestion.text).toBe(
|
||||
'<p>Quiz 1, question 2 with audio but no timestamps</p>'
|
||||
);
|
||||
|
||||
// Third question has no audio
|
||||
expect(thirdQuestion.audioData).toBeUndefined();
|
||||
expect(thirdQuestion.text).toBe('<p>Quiz 1, question 3 without audio</p>');
|
||||
});
|
||||
|
||||
it('should throw error for audio section not in JSON code block', () => {
|
||||
expect(() => plugin(quizzesWithAudioNotJsonCodeBlockAST, file)).toThrow(
|
||||
'--audio-- section must contain a ```json code block'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for invalid JSON in audio section', () => {
|
||||
expect(() => plugin(quizzesWithAudioInvalidJsonAST, file)).toThrow(
|
||||
'--audio-- section must contain valid JSON'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for missing audio filename', () => {
|
||||
expect(() => plugin(quizzesWithAudioMissingFilenameAST, file)).toThrow(
|
||||
'--audio-- section must contain audio.filename'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for transcript not being an array', () => {
|
||||
expect(() => plugin(quizzesWithAudioTranscriptNotArrayAST, file)).toThrow(
|
||||
'--audio-- section must contain transcript as an array'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for empty transcript array', () => {
|
||||
expect(() => plugin(quizzesWithAudioEmptyTranscriptAST, file)).toThrow(
|
||||
'--audio-- section transcript array cannot be empty'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for missing transcript', () => {
|
||||
expect(() => plugin(quizzesWithAudioMissingTranscriptAST, file)).toThrow(
|
||||
'--audio-- section must contain transcript as an array'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for transcript line missing character', () => {
|
||||
expect(() =>
|
||||
plugin(quizzesWithAudioTranscriptMissingCharacterAST, file)
|
||||
).toThrow(
|
||||
'--audio-- transcript line 0 must have character and text properties'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for transcript line missing text', () => {
|
||||
expect(() =>
|
||||
plugin(quizzesWithAudioTranscriptMissingTextAST, file)
|
||||
).toThrow(
|
||||
'--audio-- transcript line 0 must have character and text properties'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ const VALID_MARKERS = [
|
||||
|
||||
// Level 4
|
||||
'#### --answer--',
|
||||
'#### --audio--',
|
||||
'#### --distractors--',
|
||||
'#### --text--'
|
||||
];
|
||||
|
||||
@@ -26,6 +26,20 @@ const idToFilepath = new Map();
|
||||
// recently overwritten files
|
||||
const idToOverwrittenFile = new Map();
|
||||
|
||||
exports.createSchemaCustomization = ({ actions }) => {
|
||||
const { createTypes } = actions;
|
||||
const typeDefs = `
|
||||
type QuizQuestion {
|
||||
text: String!
|
||||
distractors: [String!]!
|
||||
answer: String!
|
||||
audioId: String
|
||||
transcript: String
|
||||
}
|
||||
`;
|
||||
createTypes(typeDefs);
|
||||
};
|
||||
|
||||
exports.sourceNodes = function sourceChallengesSourceNodes(
|
||||
{ actions, reporter, createNodeId, createContentDigest },
|
||||
pluginOptions
|
||||
|
||||
Reference in New Issue
Block a user