Started working on adding mobile buzzers, a lot of rewriting
This commit is contained in:
591
src/lib/stores/editor.test.ts
Normal file
591
src/lib/stores/editor.test.ts
Normal file
@@ -0,0 +1,591 @@
|
||||
/**
|
||||
* EditorStore Unit Tests
|
||||
*
|
||||
* Testing approach: Since EditorStore uses Svelte 5 runes ($state) which require
|
||||
* Svelte compilation, we test the logic by:
|
||||
* 1. Extracting and testing pure validation/helper functions
|
||||
* 2. Mocking localStorage for persistence tests
|
||||
* 3. Testing the store's public API through a mock implementation
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { GameSettings, Team, Round, FinalRound, Question, Category } from '$lib/types/kuldvillak';
|
||||
import { DEFAULT_SETTINGS } from '$lib/types/kuldvillak';
|
||||
|
||||
// ============================================
|
||||
// Test Helpers - Replicate pure functions from store
|
||||
// ============================================
|
||||
|
||||
function generateId(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
function createQuestion(points: number): Question {
|
||||
return {
|
||||
id: generateId(),
|
||||
question: "",
|
||||
answer: "",
|
||||
points,
|
||||
isDailyDouble: false,
|
||||
isRevealed: false,
|
||||
};
|
||||
}
|
||||
|
||||
function createCategory(settings: GameSettings, multiplier: number = 1): Category {
|
||||
return {
|
||||
id: generateId(),
|
||||
name: "",
|
||||
questions: settings.pointValues.map((p) => createQuestion(p * multiplier)),
|
||||
};
|
||||
}
|
||||
|
||||
function createRound(name: string, multiplier: number, settings: GameSettings): Round {
|
||||
return {
|
||||
id: generateId(),
|
||||
name,
|
||||
categories: Array.from({ length: settings.categoriesPerRound }, () =>
|
||||
createCategory(settings, multiplier)
|
||||
),
|
||||
pointMultiplier: multiplier,
|
||||
};
|
||||
}
|
||||
|
||||
// Validation function extracted for testing
|
||||
function validateGame(
|
||||
teams: Team[],
|
||||
rounds: Round[],
|
||||
settings: GameSettings,
|
||||
finalRound: FinalRound
|
||||
): string | null {
|
||||
if (teams.length < 2) {
|
||||
return "At least 2 players required";
|
||||
}
|
||||
|
||||
for (let i = 0; i < rounds.length; i++) {
|
||||
const hasQuestions = rounds[i].categories.some((cat) =>
|
||||
cat.questions.some((q) => q.question.trim())
|
||||
);
|
||||
if (!hasQuestions) {
|
||||
return `Round ${i + 1} needs at least one question`;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.enableFinalRound && !finalRound.question.trim()) {
|
||||
return "Final round question is required";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Count daily doubles helper
|
||||
function countDailyDoubles(rounds: Round[], roundIndex: number): number {
|
||||
return rounds[roundIndex]?.categories.reduce(
|
||||
(sum, cat) => sum + cat.questions.filter((q) => q.isDailyDouble).length,
|
||||
0
|
||||
) ?? 0;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Mock localStorage
|
||||
// ============================================
|
||||
|
||||
const AUTOSAVE_KEY = "kuldvillak-editor-autosave";
|
||||
|
||||
function createMockLocalStorage() {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store[key] ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => { store[key] = value; }),
|
||||
removeItem: vi.fn((key: string) => { delete store[key]; }),
|
||||
clear: vi.fn(() => { store = {}; }),
|
||||
get _store() { return store; },
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Tests
|
||||
// ============================================
|
||||
|
||||
describe('EditorStore Logic', () => {
|
||||
let mockLocalStorage: ReturnType<typeof createMockLocalStorage>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLocalStorage = createMockLocalStorage();
|
||||
vi.stubGlobal('localStorage', mockLocalStorage);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Helper Function Tests
|
||||
// ============================================
|
||||
|
||||
describe('createQuestion', () => {
|
||||
it('should create a question with correct structure', () => {
|
||||
const q = createQuestion(100);
|
||||
expect(q.points).toBe(100);
|
||||
expect(q.question).toBe("");
|
||||
expect(q.answer).toBe("");
|
||||
expect(q.isDailyDouble).toBe(false);
|
||||
expect(q.isRevealed).toBe(false);
|
||||
expect(q.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should generate unique IDs', () => {
|
||||
const q1 = createQuestion(100);
|
||||
const q2 = createQuestion(100);
|
||||
expect(q1.id).not.toBe(q2.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCategory', () => {
|
||||
it('should create category with correct number of questions', () => {
|
||||
const cat = createCategory(DEFAULT_SETTINGS, 1);
|
||||
expect(cat.questions.length).toBe(DEFAULT_SETTINGS.pointValues.length);
|
||||
});
|
||||
|
||||
it('should apply multiplier to point values', () => {
|
||||
const cat = createCategory(DEFAULT_SETTINGS, 2);
|
||||
const expectedPoints = DEFAULT_SETTINGS.pointValues.map(p => p * 2);
|
||||
cat.questions.forEach((q, i) => {
|
||||
expect(q.points).toBe(expectedPoints[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createRound', () => {
|
||||
it('should create round with correct structure', () => {
|
||||
const round = createRound("Test Round", 1, DEFAULT_SETTINGS);
|
||||
expect(round.name).toBe("Test Round");
|
||||
expect(round.pointMultiplier).toBe(1);
|
||||
expect(round.categories.length).toBe(DEFAULT_SETTINGS.categoriesPerRound);
|
||||
});
|
||||
|
||||
it('should apply multiplier to all questions', () => {
|
||||
const round = createRound("Double", 2, DEFAULT_SETTINGS);
|
||||
round.categories.forEach(cat => {
|
||||
cat.questions.forEach((q, i) => {
|
||||
expect(q.points).toBe(DEFAULT_SETTINGS.pointValues[i] * 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Validation Tests
|
||||
// ============================================
|
||||
|
||||
describe('validateGame', () => {
|
||||
let validTeams: Team[];
|
||||
let validRounds: Round[];
|
||||
let validSettings: GameSettings;
|
||||
let validFinalRound: FinalRound;
|
||||
|
||||
beforeEach(() => {
|
||||
validTeams = [
|
||||
{ id: '1', name: 'Team 1', score: 0 },
|
||||
{ id: '2', name: 'Team 2', score: 0 },
|
||||
];
|
||||
validRounds = [createRound("Round 1", 1, DEFAULT_SETTINGS)];
|
||||
// Fill at least one question
|
||||
validRounds[0].categories[0].questions[0].question = "Test question?";
|
||||
validSettings = { ...DEFAULT_SETTINGS, enableFinalRound: false };
|
||||
validFinalRound = { category: "", question: "", answer: "" };
|
||||
});
|
||||
|
||||
it('should pass with valid minimal configuration', () => {
|
||||
const result = validateGame(validTeams, validRounds, validSettings, validFinalRound);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail with fewer than 2 teams', () => {
|
||||
const result = validateGame([validTeams[0]], validRounds, validSettings, validFinalRound);
|
||||
expect(result).toBe("At least 2 players required");
|
||||
});
|
||||
|
||||
it('should fail with 0 teams', () => {
|
||||
const result = validateGame([], validRounds, validSettings, validFinalRound);
|
||||
expect(result).toBe("At least 2 players required");
|
||||
});
|
||||
|
||||
it('should fail when round has no questions', () => {
|
||||
const emptyRounds = [createRound("Empty", 1, DEFAULT_SETTINGS)];
|
||||
const result = validateGame(validTeams, emptyRounds, validSettings, validFinalRound);
|
||||
expect(result).toBe("Round 1 needs at least one question");
|
||||
});
|
||||
|
||||
it('should fail when second round has no questions', () => {
|
||||
const twoRounds = [
|
||||
validRounds[0],
|
||||
createRound("Round 2", 2, DEFAULT_SETTINGS), // No questions filled
|
||||
];
|
||||
const result = validateGame(validTeams, twoRounds, validSettings, validFinalRound);
|
||||
expect(result).toBe("Round 2 needs at least one question");
|
||||
});
|
||||
|
||||
it('should fail when final round enabled but no question', () => {
|
||||
const settingsWithFinal = { ...validSettings, enableFinalRound: true };
|
||||
const result = validateGame(validTeams, validRounds, settingsWithFinal, validFinalRound);
|
||||
expect(result).toBe("Final round question is required");
|
||||
});
|
||||
|
||||
it('should pass when final round enabled and has question', () => {
|
||||
const settingsWithFinal = { ...validSettings, enableFinalRound: true };
|
||||
const finalWithQuestion = { category: "History", question: "What year?", answer: "1990" };
|
||||
const result = validateGame(validTeams, validRounds, settingsWithFinal, finalWithQuestion);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should pass with whitespace-only question as empty', () => {
|
||||
validRounds[0].categories[0].questions[0].question = " ";
|
||||
const result = validateGame(validTeams, validRounds, validSettings, validFinalRound);
|
||||
expect(result).toBe("Round 1 needs at least one question");
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Team Management Tests
|
||||
// ============================================
|
||||
|
||||
describe('Team Management Logic', () => {
|
||||
it('canAddTeam should be true when under 6 teams', () => {
|
||||
const teams: Team[] = [
|
||||
{ id: '1', name: 'Team 1', score: 0 },
|
||||
{ id: '2', name: 'Team 2', score: 0 },
|
||||
];
|
||||
expect(teams.length < 6).toBe(true);
|
||||
});
|
||||
|
||||
it('canAddTeam should be false when at 6 teams', () => {
|
||||
const teams: Team[] = Array.from({ length: 6 }, (_, i) => ({
|
||||
id: String(i),
|
||||
name: `Team ${i + 1}`,
|
||||
score: 0,
|
||||
}));
|
||||
expect(teams.length < 6).toBe(false);
|
||||
});
|
||||
|
||||
it('canRemoveTeam should be true when over 2 teams', () => {
|
||||
const teams: Team[] = [
|
||||
{ id: '1', name: 'Team 1', score: 0 },
|
||||
{ id: '2', name: 'Team 2', score: 0 },
|
||||
{ id: '3', name: 'Team 3', score: 0 },
|
||||
];
|
||||
expect(teams.length > 2).toBe(true);
|
||||
});
|
||||
|
||||
it('canRemoveTeam should be false when at 2 teams', () => {
|
||||
const teams: Team[] = [
|
||||
{ id: '1', name: 'Team 1', score: 0 },
|
||||
{ id: '2', name: 'Team 2', score: 0 },
|
||||
];
|
||||
expect(teams.length > 2).toBe(false);
|
||||
});
|
||||
|
||||
it('addTeam should generate unique ID', () => {
|
||||
const teams: Team[] = [];
|
||||
const newTeam = { id: generateId(), name: 'New Team', score: 0 };
|
||||
teams.push(newTeam);
|
||||
expect(newTeam.id).toBeDefined();
|
||||
expect(typeof newTeam.id).toBe('string');
|
||||
expect(newTeam.id.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('removeTeam should filter by ID', () => {
|
||||
const teams: Team[] = [
|
||||
{ id: '1', name: 'Team 1', score: 0 },
|
||||
{ id: '2', name: 'Team 2', score: 0 },
|
||||
{ id: '3', name: 'Team 3', score: 0 },
|
||||
];
|
||||
const filtered = teams.filter(t => t.id !== '2');
|
||||
expect(filtered.length).toBe(2);
|
||||
expect(filtered.find(t => t.id === '2')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Daily Double Tests
|
||||
// ============================================
|
||||
|
||||
describe('Daily Double Logic', () => {
|
||||
it('countDailyDoubles should return 0 for round with no DDs', () => {
|
||||
const rounds = [createRound("Test", 1, DEFAULT_SETTINGS)];
|
||||
expect(countDailyDoubles(rounds, 0)).toBe(0);
|
||||
});
|
||||
|
||||
it('countDailyDoubles should count correctly', () => {
|
||||
const rounds = [createRound("Test", 1, DEFAULT_SETTINGS)];
|
||||
rounds[0].categories[0].questions[0].isDailyDouble = true;
|
||||
rounds[0].categories[1].questions[2].isDailyDouble = true;
|
||||
expect(countDailyDoubles(rounds, 0)).toBe(2);
|
||||
});
|
||||
|
||||
it('should respect maxDD limit logic', () => {
|
||||
const maxDD = 2;
|
||||
const currentDD = 2;
|
||||
const canAddMore = currentDD < maxDD;
|
||||
expect(canAddMore).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow toggle when under limit', () => {
|
||||
const maxDD = 2;
|
||||
const currentDD = 1;
|
||||
const canAddMore = currentDD < maxDD;
|
||||
expect(canAddMore).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Persistence Tests
|
||||
// ============================================
|
||||
|
||||
describe('Persistence Logic', () => {
|
||||
it('save should serialize data to localStorage', () => {
|
||||
const data = {
|
||||
name: "Test Game",
|
||||
settings: DEFAULT_SETTINGS,
|
||||
teams: [{ id: '1', name: 'Team 1', score: 0 }],
|
||||
rounds: [],
|
||||
finalRound: { category: "", question: "", answer: "" },
|
||||
};
|
||||
|
||||
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(data));
|
||||
|
||||
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
||||
AUTOSAVE_KEY,
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
it('load should parse valid localStorage data', () => {
|
||||
const savedData = {
|
||||
name: "Saved Game",
|
||||
settings: { ...DEFAULT_SETTINGS, defaultTimerSeconds: 10 },
|
||||
teams: [
|
||||
{ id: '1', name: 'Player 1', score: 100 },
|
||||
{ id: '2', name: 'Player 2', score: 200 },
|
||||
],
|
||||
rounds: [],
|
||||
finalRound: { category: "History", question: "Q?", answer: "A" },
|
||||
};
|
||||
|
||||
mockLocalStorage._store[AUTOSAVE_KEY] = JSON.stringify(savedData);
|
||||
const loaded = JSON.parse(localStorage.getItem(AUTOSAVE_KEY)!);
|
||||
|
||||
expect(loaded.name).toBe("Saved Game");
|
||||
expect(loaded.settings.defaultTimerSeconds).toBe(10);
|
||||
expect(loaded.teams.length).toBe(2);
|
||||
});
|
||||
|
||||
it('load should handle invalid JSON gracefully', () => {
|
||||
mockLocalStorage._store[AUTOSAVE_KEY] = "not valid json {{{";
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
JSON.parse(localStorage.getItem(AUTOSAVE_KEY)!);
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
});
|
||||
|
||||
it('load should handle missing data gracefully', () => {
|
||||
const result = localStorage.getItem(AUTOSAVE_KEY);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('clearSaved should remove from localStorage', () => {
|
||||
mockLocalStorage._store[AUTOSAVE_KEY] = "some data";
|
||||
localStorage.removeItem(AUTOSAVE_KEY);
|
||||
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(AUTOSAVE_KEY);
|
||||
});
|
||||
|
||||
it('should handle legacy teamColors property', () => {
|
||||
const legacyData = {
|
||||
name: "Legacy Game",
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
teamColors: ['red', 'blue'], // Legacy property
|
||||
},
|
||||
teams: [],
|
||||
rounds: [],
|
||||
finalRound: { category: "", question: "", answer: "" },
|
||||
};
|
||||
|
||||
mockLocalStorage._store[AUTOSAVE_KEY] = JSON.stringify(legacyData);
|
||||
const loaded = JSON.parse(localStorage.getItem(AUTOSAVE_KEY)!);
|
||||
|
||||
// Should be able to destructure and remove teamColors
|
||||
const { teamColors, ...cleanSettings } = loaded.settings;
|
||||
expect(teamColors).toEqual(['red', 'blue']);
|
||||
expect(cleanSettings.teamColors).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Export/Import Tests
|
||||
// ============================================
|
||||
|
||||
describe('Export/Import Logic', () => {
|
||||
it('exportGame should create valid JSON structure', () => {
|
||||
const gameData = {
|
||||
name: "Export Test",
|
||||
settings: DEFAULT_SETTINGS,
|
||||
teams: [
|
||||
{ id: '1', name: 'Team 1', score: 0 },
|
||||
{ id: '2', name: 'Team 2', score: 0 },
|
||||
],
|
||||
rounds: [createRound("Round 1", 1, DEFAULT_SETTINGS)],
|
||||
finalRound: { category: "Cat", question: "Q?", answer: "A" },
|
||||
};
|
||||
|
||||
const json = JSON.stringify(gameData, null, 2);
|
||||
const parsed = JSON.parse(json);
|
||||
|
||||
expect(parsed.name).toBe("Export Test");
|
||||
expect(parsed.teams.length).toBe(2);
|
||||
expect(parsed.rounds.length).toBe(1);
|
||||
});
|
||||
|
||||
it('exportGame should create Blob with correct type', () => {
|
||||
const json = JSON.stringify({ test: true });
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
|
||||
expect(blob.type).toBe("application/json");
|
||||
expect(blob.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('importGame should validate required fields', () => {
|
||||
const validData = {
|
||||
settings: DEFAULT_SETTINGS,
|
||||
teams: [],
|
||||
rounds: [],
|
||||
};
|
||||
|
||||
const invalidData1 = { teams: [], rounds: [] }; // Missing settings
|
||||
const invalidData2 = { settings: DEFAULT_SETTINGS, rounds: [] }; // Missing teams
|
||||
const invalidData3 = { settings: DEFAULT_SETTINGS, teams: [] }; // Missing rounds
|
||||
|
||||
const hasRequired = (data: Record<string, unknown>) =>
|
||||
!!(data.settings && data.teams && data.rounds);
|
||||
|
||||
expect(hasRequired(validData)).toBe(true);
|
||||
expect(hasRequired(invalidData1)).toBe(false);
|
||||
expect(hasRequired(invalidData2)).toBe(false);
|
||||
expect(hasRequired(invalidData3)).toBe(false);
|
||||
});
|
||||
|
||||
it('should generate sanitized filename', () => {
|
||||
const gameName = "My Test Game 2024";
|
||||
const filename = `${gameName.replace(/\s+/g, "_") || "game"}.json`;
|
||||
expect(filename).toBe("My_Test_Game_2024.json");
|
||||
});
|
||||
|
||||
it('should use default filename when name is empty', () => {
|
||||
const gameName = "";
|
||||
const filename = `${gameName.replace(/\s+/g, "_") || "game"}.json`;
|
||||
expect(filename).toBe("game.json");
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Round Management Tests
|
||||
// ============================================
|
||||
|
||||
describe('Round Management Logic', () => {
|
||||
it('setRoundCount should reduce to 1 round', () => {
|
||||
let rounds = [
|
||||
createRound("Round 1", 1, DEFAULT_SETTINGS),
|
||||
createRound("Round 2", 2, DEFAULT_SETTINGS),
|
||||
];
|
||||
|
||||
if (rounds.length > 1) {
|
||||
rounds = [rounds[0]];
|
||||
}
|
||||
|
||||
expect(rounds.length).toBe(1);
|
||||
});
|
||||
|
||||
it('setRoundCount should add second round', () => {
|
||||
let rounds = [createRound("Round 1", 1, DEFAULT_SETTINGS)];
|
||||
|
||||
if (rounds.length === 1) {
|
||||
rounds = [...rounds, createRound("Round 2", 2, DEFAULT_SETTINGS)];
|
||||
}
|
||||
|
||||
expect(rounds.length).toBe(2);
|
||||
expect(rounds[1].pointMultiplier).toBe(2);
|
||||
});
|
||||
|
||||
it('getPointsForRound should apply multiplier', () => {
|
||||
const base = [10, 20, 30, 40, 50];
|
||||
const round1Points = base.map(v => v * 1);
|
||||
const round2Points = base.map(v => v * 2);
|
||||
|
||||
expect(round1Points).toEqual([10, 20, 30, 40, 50]);
|
||||
expect(round2Points).toEqual([20, 40, 60, 80, 100]);
|
||||
});
|
||||
|
||||
it('updatePreset should reset to default values', () => {
|
||||
let settings = { ...DEFAULT_SETTINGS, pointValues: [5, 10, 15, 20, 25] };
|
||||
|
||||
if (settings.pointValuePreset === "round1") {
|
||||
settings.pointValues = [10, 20, 30, 40, 50];
|
||||
}
|
||||
|
||||
// Simulate preset change
|
||||
settings.pointValuePreset = "round1";
|
||||
settings.pointValues = [10, 20, 30, 40, 50];
|
||||
|
||||
expect(settings.pointValues).toEqual([10, 20, 30, 40, 50]);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Derived State Tests
|
||||
// ============================================
|
||||
|
||||
describe('Derived State Logic', () => {
|
||||
it('totalQuestions should count all questions', () => {
|
||||
const rounds = [
|
||||
createRound("R1", 1, DEFAULT_SETTINGS),
|
||||
createRound("R2", 2, DEFAULT_SETTINGS),
|
||||
];
|
||||
|
||||
const total = rounds.reduce(
|
||||
(total, round) =>
|
||||
total + round.categories.reduce(
|
||||
(catTotal, cat) => catTotal + cat.questions.length,
|
||||
0
|
||||
),
|
||||
0
|
||||
);
|
||||
|
||||
const expectedPerRound = DEFAULT_SETTINGS.categoriesPerRound * DEFAULT_SETTINGS.questionsPerCategory;
|
||||
expect(total).toBe(expectedPerRound * 2);
|
||||
});
|
||||
|
||||
it('filledQuestions should count only non-empty', () => {
|
||||
const rounds = [createRound("R1", 1, DEFAULT_SETTINGS)];
|
||||
rounds[0].categories[0].questions[0].question = "Filled";
|
||||
rounds[0].categories[0].questions[1].question = "Also filled";
|
||||
rounds[0].categories[1].questions[0].question = "Third";
|
||||
|
||||
const filled = rounds.reduce(
|
||||
(total, round) =>
|
||||
total + round.categories.reduce(
|
||||
(catTotal, cat) =>
|
||||
catTotal + cat.questions.filter((q) => q.question.trim()).length,
|
||||
0
|
||||
),
|
||||
0
|
||||
);
|
||||
|
||||
expect(filled).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user