Skip to content
Open
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
156 changes: 156 additions & 0 deletions src/__tests__/parsers/pt9/interlinearSetupXmlParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/// <reference types="jest" />

import * as fs from 'node:fs';
import * as path from 'node:path';

import { InterlinearSetupXmlParser } from 'parsers/pt9/interlinearSetupXmlParser';

describe('InterlinearSetupXmlParser', () => {
let parser: InterlinearSetupXmlParser;

beforeEach(() => {
parser = new InterlinearSetupXmlParser();
});

describe('parse() - valid XML', () => {
it('parses a setup with every field populated', () => {
const xml = `
<InterlinearSetupList>
<InterlinearSetup type="BackTranslation" language="fr">
<LanguageName>French</LanguageName>
<FontName>Arial</FontName>
<FontSize>10</FontSize>
<RightToLeft>false</RightToLeft>
<RelatedLanguages>true</RelatedLanguages>
<ExportOnApprove>true</ExportOnApprove>
<MdlScrTextName>MDL</MdlScrTextName>
<MdlScrTextId>1234567890abcdef</MdlScrTextId>
<MdlIsResource>true</MdlIsResource>
<ExportScrTextName>BT1</ExportScrTextName>
<ExportScrTextId>fedcba0987654321</ExportScrTextId>
</InterlinearSetup>
</InterlinearSetupList>
`;

expect(parser.parse(xml)).toStrictEqual({
Setups: [
{
Type: 'BackTranslation',
LanguageId: 'fr',
LanguageName: 'French',
FontName: 'Arial',
FontSize: '10',
RightToLeft: false,
RelatedLanguages: true,
ExportOnApprove: true,
MdlScrTextName: 'MDL',
MdlScrTextId: '1234567890abcdef',
MdlIsResource: true,
ExportScrTextName: 'BT1',
ExportScrTextId: 'fedcba0987654321',
},
],
});
});

it('parses an empty root element as no setups', () => {
expect(parser.parse('<InterlinearSetupList />')).toStrictEqual({ Setups: [] });
});

it('parses a root with no InterlinearSetup children as no setups', () => {
expect(parser.parse('<InterlinearSetupList><dummy /></InterlinearSetupList>')).toStrictEqual({
Setups: [],
});
});

it('parses an empty InterlinearSetup element as a setup with no fields', () => {
const xml = `
<InterlinearSetupList>
<InterlinearSetup />
</InterlinearSetupList>
`;
expect(parser.parse(xml)).toStrictEqual({ Setups: [{}] });
});

it('keeps absent fields absent on a setup with attributes only', () => {
const xml = `
<InterlinearSetupList>
<InterlinearSetup type="Glossing" language="en"></InterlinearSetup>
</InterlinearSetupList>
`;
expect(parser.parse(xml)).toStrictEqual({
Setups: [{ Type: 'Glossing', LanguageId: 'en' }],
});
});

it('parses an unrecognized boolean element text as false', () => {
const xml = `
<InterlinearSetupList>
<InterlinearSetup>
<RightToLeft>maybe</RightToLeft>
</InterlinearSetup>
</InterlinearSetupList>
`;
expect(parser.parse(xml).Setups[0].RightToLeft).toBe(false);
});

it('parses an unknown interlinear type name as its raw string', () => {
const xml = `
<InterlinearSetupList>
<InterlinearSetup type="FutureType" language="en" />
</InterlinearSetupList>
`;
expect(parser.parse(xml).Setups[0].Type).toBe('FutureType');
});

it('parses the real test-data setup fixture', () => {
const xmlPath = path.join(
__dirname,
'..',
'..',
'..',
'..',
'test-data',
'InterlinearSetup.xml',
);
const result = parser.parse(fs.readFileSync(xmlPath, 'utf-8'));

expect(result.Setups).toStrictEqual([
{
Type: 'Glossing',
LanguageId: 'en',
LanguageName: 'English',
FontName: 'Charis SIL',
FontSize: '12',
RightToLeft: false,
RelatedLanguages: false,
ExportOnApprove: false,
},
{
Type: 'BackTranslation',
LanguageId: 'fr',
LanguageName: 'French',
MdlScrTextName: 'MDL',
MdlScrTextId: '1234567890abcdef',
MdlIsResource: true,
ExportOnApprove: true,
ExportScrTextName: 'BT1',
ExportScrTextId: 'fedcba0987654321',
},
]);
});
});

describe('parse() - invalid XML / errors', () => {
it('throws when the InterlinearSetupList root element is absent', () => {
expect(() => parser.parse('<OtherRoot />')).toThrow(
expect.objectContaining({
name: 'SyntaxError',
message: expect.stringContaining(
'Invalid XML: Missing InterlinearSetupList root element',
),
}),
);
});
});
});
106 changes: 106 additions & 0 deletions src/__tests__/parsers/pt9/lexemeKey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/// <reference types="jest" />

import {
composeLexemeKeyId,
LexemeKeyData,
lexemeKeysEqual,
parseLexemeKeyId,
} from 'parsers/pt9/lexemeKey';

describe('parseLexemeKeyId', () => {
it('parses a plain id with no homograph suffix', () => {
expect(parseLexemeKeyId('Word:hello')).toStrictEqual({ Type: 'Word', Form: 'hello' });
});

it('parses a trailing :digits segment as the homograph', () => {
expect(parseLexemeKeyId('Word:a:2')).toStrictEqual({ Type: 'Word', Form: 'a', Homograph: 2 });
});

it('parses an explicit :1 suffix as homograph 1', () => {
expect(parseLexemeKeyId('Word:a:1')).toStrictEqual({ Type: 'Word', Form: 'a', Homograph: 1 });
});

it('keeps interior colons in the form and reads only the trailing digits as homograph', () => {
expect(parseLexemeKeyId('Stem:foo:bar:3')).toStrictEqual({
Type: 'Stem',
Form: 'foo:bar',
Homograph: 3,
});
});

it('keeps a non-digit trailing segment in the form', () => {
expect(parseLexemeKeyId('Word:a:b')).toStrictEqual({ Type: 'Word', Form: 'a:b' });
});

it('parses an empty form', () => {
expect(parseLexemeKeyId('Word:')).toStrictEqual({ Type: 'Word', Form: '' });
});

it('parses a form containing spaces (phrase lexemes)', () => {
expect(parseLexemeKeyId('Phrase:hello world')).toStrictEqual({
Type: 'Phrase',
Form: 'hello world',
});
});

it.each(['hello', '', ':x', 'Word-x'])('returns undefined for non-matching id "%s"', (id) => {
expect(parseLexemeKeyId(id)).toBeUndefined();
});
});

describe('composeLexemeKeyId', () => {
it('omits an absent homograph', () => {
expect(composeLexemeKeyId({ Type: 'Word', Form: 'hello' })).toBe('Word:hello');
});

it('omits homograph 1', () => {
expect(composeLexemeKeyId({ Type: 'Word', Form: 'hello', Homograph: 1 })).toBe('Word:hello');
});

it('appends a homograph greater than 1', () => {
expect(composeLexemeKeyId({ Type: 'Word', Form: 'a', Homograph: 2 })).toBe('Word:a:2');
});

it('produces an id that re-parses with a :digits form tail read as the homograph', () => {
const key: LexemeKeyData = { Type: 'Word', Form: 'a:1' };
expect(parseLexemeKeyId(composeLexemeKeyId(key))).toStrictEqual({
Type: 'Word',
Form: 'a',
Homograph: 1,
});
});
});

describe('lexemeKeysEqual', () => {
it('treats identical keys as equal', () => {
expect(
lexemeKeysEqual(
{ Type: 'Word', Form: 'a', Homograph: 2 },
{ Type: 'Word', Form: 'a', Homograph: 2 },
),
).toBe(true);
});

it('treats an absent homograph as homograph 1 on either side', () => {
expect(
lexemeKeysEqual({ Type: 'Word', Form: 'a' }, { Type: 'Word', Form: 'a', Homograph: 1 }),
).toBe(true);
expect(
lexemeKeysEqual({ Type: 'Word', Form: 'a', Homograph: 1 }, { Type: 'Word', Form: 'a' }),
).toBe(true);
});

it('distinguishes types', () => {
expect(lexemeKeysEqual({ Type: 'Word', Form: 'a' }, { Type: 'Stem', Form: 'a' })).toBe(false);
});

it('distinguishes forms', () => {
expect(lexemeKeysEqual({ Type: 'Word', Form: 'a' }, { Type: 'Word', Form: 'b' })).toBe(false);
});

it('distinguishes homographs', () => {
expect(
lexemeKeysEqual({ Type: 'Word', Form: 'a' }, { Type: 'Word', Form: 'a', Homograph: 2 }),
).toBe(false);
});
});
Loading