react-crossword-v2
Version:
A flexible, responsive, and easy-to-use crossword component for React apps
441 lines (386 loc) • 10.9 kB
text/typescript
import { jest } from '@jest/globals';
import '@testing-library/jest-dom/extend-expect';
import { CluesData, GridData } from '../types';
import {
isAcross,
otherDirection,
byNumber,
calculateExtents,
createEmptyGrid,
createGridData,
fillClues,
serializeGuesses,
deserializeGuesses,
findCorrectAnswers,
saveGuesses,
loadGuesses,
clearGuesses,
GuessData,
} from '../util';
// afterEach(cleanup);
describe('isAcross()', () => {
it('returns true for "across"', () => {
expect(isAcross('across')).toBeTruthy();
});
it('returns false for "down"', () => {
expect(isAcross('down')).toBeFalsy();
});
});
describe('otherDirection()', () => {
it('returns "down" for "across"', () => {
expect(otherDirection('across')).toBe('down');
});
it('returns "across" for "down"', () => {
expect(otherDirection('down')).toBe('across');
});
});
const one = { number: '1' };
const two = { number: '2' };
describe('byNumber()', () => {
it('returns 0 when a == b', () => {
const result = byNumber(one, one);
expect(result).toBe(0);
});
it('returns <0 when a < b', () => {
const result = byNumber(one, two);
expect(result).toBeLessThan(0);
});
it('returns >0 when a > b', () => {
const result = byNumber(two, one);
expect(result).toBeGreaterThan(0);
});
});
const simpleData = {
across: {
1: {
clue: 'one plus one',
answer: 'TWO',
row: 0,
col: 0,
},
},
down: {
2: {
clue: 'three minus two',
answer: 'ONE',
row: 0,
col: 2,
},
},
};
describe('calculateExtents()', () => {
it('applies across length to col', () => {
const result = calculateExtents(simpleData, 'across');
expect(result).toEqual({ row: 0, col: 2 });
});
it('applies down length to row', () => {
const result = calculateExtents(simpleData, 'down');
expect(result).toEqual({ row: 2, col: 2 });
});
it('handles "descending" positions in answers', () => {
// We're really doing this to exersize the !(primary > primaryMax) case!
const result = calculateExtents(
{
across: {
1: { row: 0, col: 3, answer: 'XX', clue: '' },
2: { row: 3, col: 0, answer: 'YY', clue: '' },
},
down: {},
},
'across'
);
expect(result).toEqual({ row: 3, col: 4 });
});
});
describe('createEmptyGrid()', () => {
it('creates a row-major array', () => {
const result = createEmptyGrid(2);
expect(result[0][1]).toMatchObject({ row: 0, col: 1 });
expect(result[1][0]).toMatchObject({ row: 1, col: 0 });
});
});
describe('createGridData()', () => {
it('creates grid data', () => {
const { size, gridData, clues } = createGridData(simpleData);
const expectedData: GridData = [
[
{
row: 0,
col: 0,
used: true,
number: '1',
answer: 'T',
across: '1',
},
{
row: 0,
col: 1,
used: true,
answer: 'W',
across: '1',
},
{
row: 0,
col: 2,
used: true,
number: '2',
answer: 'O',
across: '1',
down: '2',
},
],
[
{
row: 1,
col: 0,
used: false,
},
{
row: 1,
col: 1,
used: false,
},
{
row: 1,
col: 2,
used: true,
answer: 'N',
down: '2',
},
],
[
{
row: 2,
col: 0,
used: false,
},
{
row: 2,
col: 1,
used: false,
},
{
row: 2,
col: 2,
used: true,
answer: 'E',
down: '2',
},
],
];
const expectedClues = {
across: [
{
number: '1',
clue: simpleData.across[1].clue,
row: 0,
col: 0,
answer: 'TWO',
},
],
down: [
{
number: '2',
clue: simpleData.down[2].clue,
row: 0,
col: 2,
answer: 'ONE',
},
],
};
expect(size).toBe(3);
expect(gridData).toEqual(expectedData);
expect(clues).toEqual(expectedClues);
});
});
describe('fillClues()', () => {
it('fillClues can fill across', () => {
const gridData = createEmptyGrid(3);
const clues: CluesData = { across: [], down: [] };
fillClues(gridData, clues, simpleData, 'across');
expect(gridData[0][0].used).toBeTruthy();
if (gridData[0][0].used) {
expect(gridData[0][0].answer).toBe('T');
expect(gridData[0][0].across).toBe('1');
}
expect(gridData[0][1].used).toBeTruthy();
if (gridData[0][1].used) {
expect(gridData[0][1].answer).toBe('W');
expect(gridData[0][1].across).toBe('1');
}
expect(gridData[0][2].used).toBeTruthy();
if (gridData[0][2].used) {
expect(gridData[0][2].answer).toBe('O');
expect(gridData[0][2].across).toBe('1');
}
expect(clues).toEqual({
across: [
{ clue: 'one plus one', number: '1', row: 0, col: 0, answer: 'TWO' },
],
down: [],
});
});
it('fillClues can fill down', () => {
const gridData = createEmptyGrid(3);
const clues: CluesData = { across: [], down: [] };
fillClues(gridData, clues, simpleData, 'down');
expect(gridData[0][2].used).toBeTruthy();
if (gridData[0][2].used) {
expect(gridData[0][2].answer).toBe('O');
expect(gridData[0][2].down).toBe('2');
}
expect(gridData[1][2].used).toBeTruthy();
if (gridData[1][2].used) {
expect(gridData[1][2].answer).toBe('N');
expect(gridData[1][2].down).toBe('2');
}
expect(gridData[2][2].used).toBeTruthy();
if (gridData[2][2].used) {
expect(gridData[2][2].answer).toBe('E');
expect(gridData[2][2].down).toBe('2');
}
expect(clues).toEqual({
across: [],
down: [
{ clue: 'three minus two', number: '2', row: 0, col: 2, answer: 'ONE' },
],
});
});
});
describe('serializeGuesses()', () => {
it('creates expected data', () => {
const result = serializeGuesses([
[{ guess: 'A' }, { guess: '' }, { guess: '' }],
[{ guess: '' }, { guess: '' }, { guess: 'B' }],
[{ guess: '' }, { guess: 'C' }, { guess: '' }],
]);
expect(result).toEqual({ '0_0': 'A', '1_2': 'B', '2_1': 'C' });
});
});
describe('deserializeGuesses()', () => {
it('writes expected data', () => {
const gridData = [
[{ guess: '' }, { guess: '' }, { guess: '' }],
[{ guess: '' }, { guess: '' }, { guess: '' }],
[{ guess: '' }, { guess: '' }, { guess: '' }],
];
const guesses = { '0_0': 'A', '1_2': 'B', '2_1': 'C' };
deserializeGuesses(gridData, guesses);
expect(gridData).toEqual([
[{ guess: 'A' }, { guess: '' }, { guess: '' }],
[{ guess: '' }, { guess: '' }, { guess: 'B' }],
[{ guess: '' }, { guess: 'C' }, { guess: '' }],
]);
});
it('ignores out-of-range guesses', () => {
const gridData = [[{ guess: '' }]];
const guesses = { '0_0': 'A', '1_2': 'B', '2_1': 'C' };
deserializeGuesses(gridData, guesses);
expect(gridData).toEqual([[{ guess: 'A' }]]);
});
});
describe('findCorrectAnswers()', () => {
it('finds correct answers', () => {
const gridData = [
[{ guess: '' }, { guess: '' }, { guess: 'O' }],
[{ guess: '' }, { guess: '' }, { guess: 'N' }],
[{ guess: '' }, { guess: '' }, { guess: 'E' }],
];
const result = findCorrectAnswers(simpleData, gridData);
expect(result).toEqual([['down', '2', 'ONE']]);
});
});
describe('localStorage', () => {
const storageKey = 'DUMMY';
const gridData: GuessData = [[{ guess: 'X' }]];
let mockStorage: jest.SpyInstance<Storage, []>;
const setItem = jest.fn<void, [string]>();
const getItem = jest
.fn<string | null, [string]>()
.mockReturnValue(JSON.stringify({ guesses: { '0_0': 'X' } }));
const removeItem = jest.fn<void, [string]>();
beforeEach(() => {
setItem.mockClear();
getItem.mockClear();
removeItem.mockClear();
// @ts-ignore -- 'MockFunctionResult[]' is not assignable to type
// 'MockResult<Storage>[]' error!
mockStorage = jest.spyOn(window, 'localStorage', 'get');
});
afterEach(() => {
mockStorage.mockRestore();
});
function withStorage() {
// unused props...
const length = 0;
// eslint-disable-next-line @typescript-eslint/no-empty-function
const clear = () => {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const key = (index: number) => null;
mockStorage.mockReturnValue({
setItem,
getItem,
removeItem,
length,
clear,
key,
});
}
function withoutStorage() {
// @ts-ignore -- need 'undefined' to fake "no storage" condition
mockStorage.mockReturnValue(undefined);
}
describe('saveGuesses()', () => {
it("doesn't fail when localStorage is unavailable", () => {
withoutStorage();
saveGuesses(gridData, storageKey);
expect(setItem).toHaveBeenCalledTimes(0);
});
it('calls setItem when localStorage exists', () => {
withStorage();
saveGuesses(gridData, storageKey);
expect(setItem).toHaveBeenCalledTimes(1);
expect(setItem).toHaveBeenCalledWith(
storageKey,
expect.stringContaining('guesses')
);
});
});
describe('loadGuesses()', () => {
it("doesn't fail when localStorage is unavailable", () => {
withoutStorage();
loadGuesses(gridData, storageKey);
expect(getItem).toHaveBeenCalledTimes(0);
});
it('calls getItem when localStorage exists', () => {
withStorage();
const localData = createEmptyGrid(1);
loadGuesses(localData, storageKey);
expect(getItem).toHaveBeenCalledTimes(1);
expect(getItem).toHaveBeenCalledWith(storageKey);
expect(localData).toMatchObject(gridData);
});
it("doesn't alter gridData when nothing is found", () => {
withStorage();
getItem.mockReturnValue(null);
const localData = createEmptyGrid(1);
loadGuesses(localData, storageKey);
expect(getItem).toHaveBeenCalledTimes(1);
expect(getItem).toHaveBeenCalledWith(storageKey);
expect(localData).not.toMatchObject(gridData);
});
});
describe('clearGuesses()', () => {
it("doesn't fail when localStorage is unavailable", () => {
withoutStorage();
clearGuesses(storageKey);
expect(removeItem).toHaveBeenCalledTimes(0);
});
it('calls removeItem when localStorage exists', () => {
withStorage();
clearGuesses(storageKey);
expect(removeItem).toHaveBeenCalledTimes(1);
expect(removeItem).toHaveBeenCalledWith(storageKey);
});
});
});