mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-03 11:07:00 -04:00
523 lines
21 KiB
TypeScript
523 lines
21 KiB
TypeScript
import { beforeEach, describe, expect, test } from 'vitest';
|
||
|
||
import type { NullPronounsConfig } from '~/locale/config.ts';
|
||
import type { Translations } from '~/locale/translations.ts';
|
||
import { buildPronoun, buildPronounUsage } from '~/src/buildPronoun.ts';
|
||
import { PronounGroup, Pronoun, PronounLibrary } from '~/src/classes.ts';
|
||
import { Translator } from '~/src/translator.ts';
|
||
import { configWithPronouns } from '~/test/fixtures/config.ts';
|
||
import pronounsFactory, { generated as generatedPronouns } from '~/test/fixtures/pronouns.ts';
|
||
|
||
const translations: Translations = {};
|
||
|
||
beforeEach(() => {
|
||
translations.pronouns = {
|
||
any: {
|
||
short: 'any',
|
||
},
|
||
slashes: {
|
||
plural: 'plural',
|
||
pluralHonorific: 'plural-honorific',
|
||
description: 'description',
|
||
},
|
||
or: 'or',
|
||
};
|
||
});
|
||
|
||
const translator = new Translator(translations, translations, configWithPronouns);
|
||
|
||
let config = structuredClone(configWithPronouns);
|
||
const pronouns = Object.fromEntries(Object.entries(pronounsFactory)
|
||
.map(([name, pronounFactory]) => [name, pronounFactory(config)]));
|
||
|
||
beforeEach(() => {
|
||
config = structuredClone(configWithPronouns);
|
||
});
|
||
|
||
test('finds pronouns by canonical name', () => {
|
||
const actual = buildPronoun(pronouns, 'they', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toBe(pronouns.they);
|
||
});
|
||
test('finds pronouns case insensitively by canonical name', () => {
|
||
const actual = buildPronoun(pronouns, 'They', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toBe(pronouns.they);
|
||
});
|
||
|
||
test('finds pronouns by alias', () => {
|
||
const actual = buildPronoun(pronouns, 'they/them', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toBe(pronouns.they);
|
||
});
|
||
|
||
test('finds pronouns case insensitively by alias', () => {
|
||
const actual = buildPronoun(pronouns, 'They/Them', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toBe(pronouns.they);
|
||
});
|
||
|
||
const emojiPronounsConfig = {
|
||
description: 'Emojiself pronouns',
|
||
history: 'Emojiself pronouns are intended for online communication and not supposed to be pronounced',
|
||
morphemes: {
|
||
pronoun_subject: '#',
|
||
pronoun_object: '#',
|
||
possessive_determiner: '#\'s',
|
||
possessive_pronoun: '#\'s',
|
||
reflexive: '#self',
|
||
},
|
||
examples: ['💫', '💙'],
|
||
};
|
||
|
||
describe('when configured that emojiself pronouns are available', () => {
|
||
beforeEach(() => {
|
||
config.pronouns.emoji = emojiPronounsConfig;
|
||
});
|
||
|
||
test('builds pronouns from emoji', () => {
|
||
const actual = buildPronoun(pronouns, '💙', config, translator);
|
||
expect(actual).toEqual(new Pronoun(
|
||
config,
|
||
'💙',
|
||
'Emojiself pronouns',
|
||
false,
|
||
{
|
||
pronoun_subject: '💙',
|
||
pronoun_object: '💙',
|
||
possessive_determiner: '💙\'s',
|
||
possessive_pronoun: '💙\'s',
|
||
reflexive: '💙self',
|
||
},
|
||
[false],
|
||
[false],
|
||
[],
|
||
'Emojiself pronouns are intended for online communication and not supposed to be pronounced@__generator__',
|
||
false,
|
||
));
|
||
});
|
||
});
|
||
|
||
const nullPronounsConfig: NullPronounsConfig = {
|
||
routes: ['avoiding', 'no-pronouns', 'null', 'pronounless', 'nullpronominal', 'nameself'],
|
||
morphemes: {
|
||
pronoun_subject: '#',
|
||
pronoun_object: '#',
|
||
possessive_determiner: '#\'s',
|
||
possessive_pronoun: '#\'s',
|
||
reflexive: '#self',
|
||
},
|
||
examples: [':S'],
|
||
};
|
||
|
||
describe('when configured that null pronouns are available', () => {
|
||
beforeEach(() => {
|
||
config.pronouns.null = nullPronounsConfig;
|
||
translations.pronouns.null = {
|
||
description: 'Some people prefer not using any pronouns',
|
||
};
|
||
});
|
||
|
||
const expectedDescription = '{/avoiding=avoiding} / {/no-pronouns=no-pronouns} / {/null=null} / ' +
|
||
'{/pronounless=pronounless} / {/nullpronominal=nullpronominal} / {/nameself=nameself}';
|
||
|
||
test('builds pronouns from name', () => {
|
||
const actual = buildPronoun(pronouns, ':S', config, translator);
|
||
expect(actual).toEqual(new Pronoun(
|
||
config,
|
||
'S',
|
||
expectedDescription,
|
||
false,
|
||
{
|
||
pronoun_subject: 'S',
|
||
pronoun_object: 'S',
|
||
possessive_determiner: 'S\'s',
|
||
possessive_pronoun: 'S\'s',
|
||
reflexive: 'Sself',
|
||
},
|
||
[false],
|
||
[false],
|
||
[],
|
||
'Some people prefer not using any pronouns@__generator__',
|
||
false,
|
||
null,
|
||
null,
|
||
null,
|
||
false,
|
||
true,
|
||
));
|
||
});
|
||
test('builds nothing if name too long', () => {
|
||
expect(buildPronoun(pronouns, ':Abcdefghijklmnopqrstuvwxyz0123456', config, translator)).toBeNull();
|
||
});
|
||
|
||
describe('with conditional placeholders', () => {
|
||
beforeEach(() => {
|
||
nullPronounsConfig.morphemes!.possessive_pronoun = '#/[sxzß]$/i#’|#s';
|
||
});
|
||
|
||
test.each(['S', 'Ringelnatz', 'Max', 'X'])('builds morpheme conditionally if name matches', (name) => {
|
||
expect(buildPronoun(pronouns, `:${name}`, config, translator)!.morphemes.possessive_pronoun)
|
||
.toBe(`${name}’`);
|
||
});
|
||
test.each(['Sofi', 'Xavier'])('builds morpheme by default if name does not match', (name) => {
|
||
expect(buildPronoun(pronouns, `:${name}`, config, translator)!.morphemes.possessive_pronoun)
|
||
.toBe(`${name}s`);
|
||
});
|
||
});
|
||
|
||
describe('when some morphemes are not defined in template', () => {
|
||
beforeEach(() => {
|
||
nullPronounsConfig.morphemes = {
|
||
pronoun_subject: '#',
|
||
pronoun_object: '#',
|
||
possessive_determiner: '#\'s',
|
||
possessive_pronoun: '#\'s',
|
||
};
|
||
});
|
||
test('they become null', () => {
|
||
const actual = buildPronoun(pronouns, ':S', config, translator);
|
||
expect(actual).toEqual(new Pronoun(
|
||
config,
|
||
'S',
|
||
expectedDescription,
|
||
false,
|
||
{
|
||
pronoun_subject: 'S',
|
||
pronoun_object: 'S',
|
||
possessive_determiner: 'S\'s',
|
||
possessive_pronoun: 'S\'s',
|
||
reflexive: null,
|
||
},
|
||
[false],
|
||
[false],
|
||
[],
|
||
'Some people prefer not using any pronouns@__generator__',
|
||
false,
|
||
null,
|
||
null,
|
||
null,
|
||
false,
|
||
true,
|
||
));
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('when configured that slashes contain all morphemes', () => {
|
||
beforeEach(() => {
|
||
config.pronouns.honorifics = true;
|
||
config.pronouns.generator!.slashes = true;
|
||
});
|
||
|
||
test('builds generated pronoun from all morphemes', () => {
|
||
const actual = buildPronoun(pronouns, 'ae/aer/aer/aers/aerself', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aer(config));
|
||
});
|
||
test('unescapes morphemes', () => {
|
||
const actual = buildPronoun(pronouns, 's`/he/hir/hir/hirs/hirself', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.sSlashHe(config));
|
||
});
|
||
test('builds generated pronoun from all required morphemes and plural modifier', () => {
|
||
const actual = buildPronoun(pronouns, 'ae/aer/aer/aers/aerselves/:plural', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aerPlural(config));
|
||
});
|
||
test('builds generated pronoun from all required morphemes and plural honorific modifier', () => {
|
||
const actual = buildPronoun(pronouns, 'ae/aer/aer/aers/aerselves/:plural-honorific', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aerPluralHonorific(config));
|
||
});
|
||
test('builds generated pronoun from all required morphemes and description', () => {
|
||
const actual = buildPronoun(
|
||
pronouns,
|
||
'ae/aer/aer/aers/aerself/:description=Neopronoun “ae” `/ “æ”',
|
||
config,
|
||
translator,
|
||
);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aerWithDescription(config));
|
||
});
|
||
test('builds generated pronoun from all required morphemes and modifiers', () => {
|
||
const actual = buildPronoun(
|
||
pronouns,
|
||
'ae/aer/:description=Neopronoun “ae” `/ “æ”/:plural/aer/aers/aerselves',
|
||
config,
|
||
translator,
|
||
);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aerPluralWithDescription(config));
|
||
});
|
||
test('builds generated pronoun with some morphemes empty', () => {
|
||
const actual = buildPronoun(pronouns, 'ae/aer/aer/ /aerself', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aerWithEmptyPossessivePronoun(config));
|
||
});
|
||
test('builds generated pronoun with morpheme at end empty', () => {
|
||
const actual = buildPronoun(pronouns, 'ae/aer/aer/aers/', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aerWithEmptyReflexive(config));
|
||
});
|
||
test('builds generated pronoun with some morphemes unset', () => {
|
||
const actual = buildPronoun(pronouns, 'ae/aer/aer/~/aerself', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aerWithUnsetPossessivePronoun(config));
|
||
});
|
||
test('builds nothing if morphemes are missing', () => {
|
||
expect(buildPronoun(pronouns, 'ae/aer/aer/aerself', config, translator)).toBeNull();
|
||
});
|
||
test('builds nothing if too many morphemes are given', () => {
|
||
expect(buildPronoun(pronouns, 'ae/aer/aer/aers/aerself/aer', config, translator)).toBeNull();
|
||
});
|
||
test('builds nothing if description too long', () => {
|
||
expect(buildPronoun(
|
||
pronouns,
|
||
'ae/aer/aer/aers/aerself/:description=Neopronoun “ae” `/ “æ” which is my favorite so you should use it too',
|
||
config,
|
||
translator,
|
||
)).toBeNull();
|
||
});
|
||
});
|
||
describe('when configured that slashes contain some morphemes', () => {
|
||
beforeEach(() => {
|
||
config.pronouns.generator!.slashes =
|
||
['pronoun_subject', 'pronoun_object', 'possessive_determiner', 'reflexive'];
|
||
});
|
||
|
||
test('builds generated pronoun from all required morphemes', () => {
|
||
const actual = buildPronoun(pronouns, 'ae/aer/aer/aerself', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aerWithUnsetPossessivePronoun(config));
|
||
});
|
||
test('builds nothing if morphemes are missing', () => {
|
||
expect(buildPronoun(pronouns, 'ae/aer/aer', config, translator)).toBeNull();
|
||
});
|
||
test('builds nothing if too many morphemes are given', () => {
|
||
expect(buildPronoun(pronouns, 'ae/aer/aer/aers/aerself', config, translator)).toBeNull();
|
||
});
|
||
});
|
||
describe('when configured that slashes cannot contain morphemes', () => {
|
||
beforeEach(() => {
|
||
config.pronouns.generator!.slashes = false;
|
||
});
|
||
|
||
const pathBase = 'ae/aer/aer/aers/aerself/aer';
|
||
|
||
test.each([3, 4, 5, 6])('builds nothing if %d morphemes are given', (count) => {
|
||
const path = pathBase.split('/').slice(0, count)
|
||
.join('/');
|
||
expect(buildPronoun(pronouns, path.slice(0, count), config, translator)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('building generated pronouns from commas', () => {
|
||
test('succeeds with all parts present', () => {
|
||
const actual = buildPronoun(pronouns, 'ae,aer,aer,aers,aerself,0,', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aer(config));
|
||
});
|
||
test('succeeds with description missing present', () => {
|
||
const actual = buildPronoun(pronouns, 'ae,aer,aer,aers,aerself,0', config, translator);
|
||
expect(actual).toBeDefined();
|
||
expect(actual).toEqual(generatedPronouns.aer(config));
|
||
});
|
||
test('succeeds with base pronoun and some custom morphemes', () => {
|
||
const actual = buildPronoun(pronouns, 'they,!2,aers,!2,', config, translator);
|
||
expect(actual).toEqual(new Pronoun(
|
||
config,
|
||
'they/them',
|
||
'',
|
||
false,
|
||
{
|
||
pronoun_subject: 'they',
|
||
pronoun_object: 'them',
|
||
possessive_determiner: 'their',
|
||
possessive_pronoun: 'aers',
|
||
reflexive: 'themselves',
|
||
},
|
||
[true],
|
||
[false],
|
||
[],
|
||
'__generator__',
|
||
false,
|
||
));
|
||
});
|
||
test('fails when too few parts are given', () => {
|
||
expect(buildPronoun(pronouns, 'ae,aer,aer,aers,aerself', config, translator)).toBeNull();
|
||
});
|
||
test('fails when many few parts are given', () => {
|
||
expect(buildPronoun(pronouns, 'ae,aer,aer,aers,aerself,aersing,0,', config, translator)).toBeNull();
|
||
});
|
||
test('fails when base pronoun is unknown', () => {
|
||
expect(buildPronoun(pronouns, 's/he,!2,aers,!2,', config, translator)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('builds interchangeable pronouns from ampersand', () => {
|
||
test('two interchangeable pronouns', () => {
|
||
const actual = buildPronoun(pronouns, 'he&she', config, translator);
|
||
expect(actual).toEqual(new Pronoun(
|
||
config,
|
||
'he&she',
|
||
['Normative “he/him”', 'Normative “she/her”'],
|
||
true,
|
||
{
|
||
pronoun_subject: 'he&she',
|
||
pronoun_object: 'him&her',
|
||
possessive_determiner: 'his&her',
|
||
possessive_pronoun: 'his&hers',
|
||
reflexive: 'himself&herself',
|
||
},
|
||
[false, false],
|
||
[false, false],
|
||
[],
|
||
'',
|
||
false,
|
||
));
|
||
});
|
||
test('three interchangeable pronouns', () => {
|
||
const actual = buildPronoun(pronouns, 'he&she&they', config, translator);
|
||
expect(actual).toEqual(new Pronoun(
|
||
config,
|
||
'he&she&they',
|
||
['Normative “he/him”', 'Normative “she/her”', 'Singular “they”'],
|
||
true,
|
||
{
|
||
pronoun_subject: 'he&she&they',
|
||
pronoun_object: 'him&her&them',
|
||
possessive_determiner: 'his&her&their',
|
||
possessive_pronoun: 'his&hers&theirs',
|
||
reflexive: 'himself&herself&themselves',
|
||
},
|
||
[false, false, true],
|
||
[false, false, true],
|
||
[],
|
||
'',
|
||
false,
|
||
));
|
||
});
|
||
});
|
||
|
||
describe('building the short of a pronoun usage', () => {
|
||
const pronounGroups = [new PronounGroup('Binary forms', ['he', 'she'], null, 'normative')];
|
||
const pronounLibrary = new PronounLibrary(config, pronounGroups, pronouns);
|
||
|
||
test('of a canonical pronoun', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'she', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['she/her'], glue: ' or ' }, pronoun: pronouns.she });
|
||
});
|
||
test('of a generated pronoun in slash format', () => {
|
||
config.pronouns.honorifics = true;
|
||
config.pronouns.generator!.slashes = true;
|
||
|
||
const actual = buildPronounUsage(pronounLibrary, 'ae/aer/aer/aers/aerself', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['ae/aer'], glue: ' or ' }, pronoun: generatedPronouns.aer(config) });
|
||
});
|
||
test('of interchangeable', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'he&they', config, translator);
|
||
expect(actual).toMatchObject({ short: { options: ['he/him', 'they/them'], glue: ' or ' } });
|
||
});
|
||
test('of null pronoun', () => {
|
||
config.pronouns.null = nullPronounsConfig;
|
||
|
||
const actual = buildPronounUsage(pronounLibrary, ':A', config, translator);
|
||
expect(actual).toMatchObject({ short: { options: ['A/A\'s'], glue: ' or ' } });
|
||
});
|
||
describe('of null route', () => {
|
||
beforeEach(() => {
|
||
config.pronouns.null = nullPronounsConfig;
|
||
});
|
||
|
||
test('without specific translation', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'avoiding', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['avoiding'] } });
|
||
});
|
||
test('with specific translation', () => {
|
||
translations.pronouns.null = {
|
||
short: {
|
||
'no-pronouns': 'no pronouns',
|
||
},
|
||
};
|
||
|
||
const actual = buildPronounUsage(pronounLibrary, 'no-pronouns', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['no pronouns'] } });
|
||
});
|
||
test('is case insensitive', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'Avoiding', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['avoiding'] } });
|
||
});
|
||
});
|
||
test('of emojiself', () => {
|
||
config.pronouns.emoji = emojiPronounsConfig;
|
||
|
||
const actual = buildPronounUsage(pronounLibrary, '💫', config, translator);
|
||
expect(actual).toMatchObject({ short: { options: ['💫/💫\'s'] } });
|
||
});
|
||
describe('of mirror pronouns', () => {
|
||
beforeEach(() => {
|
||
config.pronouns.mirror = {
|
||
route: 'mirror',
|
||
name: 'Mirror pronouns / Mirrorpronominal',
|
||
description: 'A person who uses mirror pronouns wants to be referred to with the same pronouns as the person talking.',
|
||
example: [
|
||
'Person A uses mirror pronouns.',
|
||
'Person B uses {/she=she/her}, so when she talks about person A, she uses “she/her” to refer to her.',
|
||
'Person C uses {/ze=ze/hir} interchangeably with {/fae=fæ/fær}, so when ze talks about person A, fæ uses either ze/hir or fæ/fær to refer to fær.',
|
||
],
|
||
};
|
||
});
|
||
|
||
test('without specific translation', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'mirror', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['mirror'] } });
|
||
});
|
||
test('with specific translation', () => {
|
||
translations.pronouns.mirror = {
|
||
short: 'mirror pronouns',
|
||
};
|
||
const actual = buildPronounUsage(pronounLibrary, 'mirror', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['mirror pronouns'] } });
|
||
});
|
||
test('is case insensitive', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'Mirror', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['mirror'] } });
|
||
});
|
||
});
|
||
describe('of any route', () => {
|
||
test('using route name', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'any', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['any'] } });
|
||
});
|
||
test('is case insensitive', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'Any', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['any'] } });
|
||
});
|
||
});
|
||
describe('of any group', () => {
|
||
test('without specific translation', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'any:normative', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['any normative'] } });
|
||
});
|
||
test('with specific translation', () => {
|
||
translations.pronouns.any.group = {
|
||
normative: {
|
||
short: 'both binaries',
|
||
},
|
||
};
|
||
|
||
const actual = buildPronounUsage(pronounLibrary, 'any:normative', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['both binaries'] } });
|
||
});
|
||
test('is case insensitive', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'Any:Normative', config, translator);
|
||
expect(actual).toEqual({ short: { options: ['any normative'] } });
|
||
});
|
||
test('of unknown yields nothing', () => {
|
||
const actual = buildPronounUsage(pronounLibrary, 'any:unknown', config, translator);
|
||
expect(actual).toBeNull();
|
||
});
|
||
});
|
||
});
|