Skip to content
Merged
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
27 changes: 11 additions & 16 deletions src/__tests__/analysis-repository-conformance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { AnalysisRepository, DraftExport } from '../types/analysis-reposito
import { emptyAnalysis, emptyDraft } from '../types/empty-factories';
import { CURRENT_MODEL_VERSION } from '../types/model-version';
import { ReadOnlyAnalysisError } from '../types/read-only-analysis-error';
import { FIXTURE_STAMPS, makePhraseLink } from './test-helpers';
import { FIXTURE_STAMPS, makePhraseLink, makeTokenLink } from './test-helpers';

/** One empty store an {@link AnalysisRepository} implementation keeps drafts in. */
export interface ConformanceStore {
Expand Down Expand Up @@ -49,16 +49,6 @@ export interface ConformanceStore {

const SOURCE = 'source-project';

function tokenLink(id: string, analysisId: string, tokenRef: string, surfaceText: string) {
return {
id,
analysisId,
...FIXTURE_STAMPS,
status: 'approved' as const,
token: { tokenRef, surfaceText },
};
}

/**
* Every record kind and every optional field, spread over GEN and EXO. One payload is linked from
* both books and one from neither. Arrays inside records are out of sorted order, so a repository
Expand Down Expand Up @@ -119,11 +109,14 @@ function fullAnalysis(): TextAnalysis {
{ id: 'ta-unlinked', ...FIXTURE_STAMPS, surfaceText: 'spare', gloss: { en: 'spare' } },
],
tokenAnalysisLinks: [
tokenLink('tl-gen-and', 'ta-and', 'GEN 1:1:0', 'and'),
tokenLink('tl-exo-and', 'ta-and', 'EXO 1:1:0', 'and'),
makeTokenLink('ta-and', 'GEN 1:1:0', { id: 'tl-gen-and', surfaceText: 'and' }),
makeTokenLink('ta-and', 'EXO 1:1:0', { id: 'tl-exo-and', surfaceText: 'and' }),
{
...tokenLink('tl-gen-beginning', 'ta-beginning', 'GEN 1:1:9', 'beginning'),
status: 'suggested',
...makeTokenLink('ta-beginning', 'GEN 1:1:9', {
id: 'tl-gen-beginning',
surfaceText: 'beginning',
status: 'suggested',
}),
confidence: 'low',
},
],
Expand Down Expand Up @@ -340,7 +333,9 @@ export function describeAnalysisRepositoryConformance(
analysis: {
...emptyAnalysis(),
tokenAnalyses: [{ id: 'ta-and', ...FIXTURE_STAMPS, surfaceText: 'and' }],
tokenAnalysisLinks: [tokenLink('tl-and', 'ta-and', 'GEN 1:1:0', 'and')],
tokenAnalysisLinks: [
makeTokenLink('ta-and', 'GEN 1:1:0', { id: 'tl-and', surfaceText: 'and' }),
],
},
};
await (await store.open(SOURCE)).replaceAll(minimal);
Expand Down
35 changes: 34 additions & 1 deletion src/__tests__/test-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { SerializedVerseRef } from '@sillsdev/scripture';
import type { ExecutionActivationContext, UseWebViewScrollGroupScrRefHook } from '@papi/core';
import type { Book, InterlinearProject, PhraseAnalysisLink, Segment, Token } from 'interlinearizer';
import type {
AssignmentStatus,
Book,
InterlinearProject,
PhraseAnalysisLink,
Segment,
Token,
TokenAnalysisLink,
} from 'interlinearizer';
import { UnsubscriberAsyncList } from 'platform-bible-utils';
import { useEffect, useState } from 'react';
import { tokenizeBook } from 'parsers/papi/bookTokenizer';
Expand Down Expand Up @@ -460,6 +468,31 @@ export function makePhraseLink(
};
}

type TokenLinkFixtureFields = {
/**
* The form the link was written against. Defaults to the token ref, which suits a fixture only
* when drift detection is not under test.
*/
surfaceText?: string;
/** Defaults to `'approved'`. */
status?: AssignmentStatus;
/** Defaults to an id that differs whenever the token ref, analysis id, or status does. */
id?: string;
};

/** Builds a token link fixture joining `tokenRef` to the payload `analysisId`. */
export function makeTokenLink(
analysisId: string,
tokenRef: string,
{
surfaceText = tokenRef,
status = 'approved',
id = JSON.stringify([tokenRef, analysisId, status]),
}: TokenLinkFixtureFields = {},
): TokenAnalysisLink {
return { ...FIXTURE_STAMPS, id, analysisId, status, token: { tokenRef, surfaceText } };
}

/**
* Reports the host as macOS to the user-agent check behind the platform's `isMacOs`, so a test can
* assert the Mac wording of a keyboard hint.
Expand Down
84 changes: 34 additions & 50 deletions src/__tests__/types/type-guards.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { AssignmentStatus, PhraseAnalysisLink, TextAnalysis } from 'interlinearizer';
import { FIXTURE_STAMPS } from '../test-helpers';
import type {
AssignmentStatus,
PhraseAnalysisLink,
TextAnalysis,
TokenAnalysisLink,
} from 'interlinearizer';
import { FIXTURE_STAMPS, makeTokenLink } from '../test-helpers';
import { emptyAnalysis } from '../../types/empty-factories';
import {
hasLinkWithoutId,
Expand Down Expand Up @@ -95,14 +100,9 @@ describe('isTextAnalysis', () => {
});

it('rejects a token link that carries no id', () => {
expect(
isTextAnalysis({
...emptyAnalysis(),
tokenAnalysisLinks: [
{ ...FIXTURE_STAMPS, analysisId: 'ta-1', status: 'approved', token: TOKEN },
],
}),
).toBe(false);
const link: Partial<TokenAnalysisLink> = makeTokenLink('ta-1', 'GEN 1:1:0');
delete link.id;
expect(isTextAnalysis({ ...emptyAnalysis(), tokenAnalysisLinks: [link] })).toBe(false);
});

it('rejects a segment link that carries no id', () => {
Expand Down Expand Up @@ -163,15 +163,7 @@ describe('isTextAnalysis', () => {
expect(
isTextAnalysis({
...emptyAnalysis(),
tokenAnalysisLinks: [
{
id: 'tl-1',
analysisId: 'ta-1',
status: 'approved',
confidence: 'certain',
token: TOKEN,
},
],
tokenAnalysisLinks: [{ ...makeTokenLink('ta-1', 'GEN 1:1:0'), confidence: 'certain' }],
}),
).toBe(false);
});
Expand All @@ -181,20 +173,6 @@ function tokenAnalysis(id: string) {
return { ...FIXTURE_STAMPS, id, surfaceText: 'word' };
}

function tokenLink(
analysisId: string,
status: AssignmentStatus = 'approved',
tokenRef = TOKEN.tokenRef,
) {
return {
...FIXTURE_STAMPS,
id: `${tokenRef}:${analysisId}:${status}`,
analysisId,
status,
token: { ...TOKEN, tokenRef },
};
}

function phraseLink(
analysisId: string,
tokenRefs: string[],
Expand Down Expand Up @@ -224,7 +202,7 @@ describe('validateTextAnalysis', () => {
validateTextAnalysis(
tokenLayer({
tokenAnalyses: [tokenAnalysis('ta-1')],
tokenAnalysisLinks: [tokenLink('ta-1')],
tokenAnalysisLinks: [makeTokenLink('ta-1', 'GEN 1:1:0')],
}),
),
).toEqual([]);
Expand All @@ -235,7 +213,10 @@ describe('validateTextAnalysis', () => {
validateTextAnalysis(
tokenLayer({
tokenAnalyses: [tokenAnalysis('ta-1'), tokenAnalysis('ta-2')],
tokenAnalysisLinks: [tokenLink('ta-1'), tokenLink('ta-2')],
tokenAnalysisLinks: [
makeTokenLink('ta-1', 'GEN 1:1:0'),
makeTokenLink('ta-2', 'GEN 1:1:0'),
],
}),
),
).toEqual([{ kind: 'multipleApproved', layer: 'token', count: 1, sample: ['GEN 1:1:0'] }]);
Expand All @@ -246,15 +227,20 @@ describe('validateTextAnalysis', () => {
validateTextAnalysis(
tokenLayer({
tokenAnalyses: [tokenAnalysis('ta-1'), tokenAnalysis('ta-2')],
tokenAnalysisLinks: [tokenLink('ta-1'), tokenLink('ta-2', 'suggested')],
tokenAnalysisLinks: [
makeTokenLink('ta-1', 'GEN 1:1:0'),
makeTokenLink('ta-2', 'GEN 1:1:0', { status: 'suggested' }),
],
}),
),
).toEqual([]);
});

it('reports a link whose analysisId names no payload', () => {
expect(
validateTextAnalysis(tokenLayer({ tokenAnalysisLinks: [tokenLink('ta-missing')] })),
validateTextAnalysis(
tokenLayer({ tokenAnalysisLinks: [makeTokenLink('ta-missing', 'GEN 1:1:0')] }),
),
).toEqual([{ kind: 'danglingLink', layer: 'token', count: 1, sample: ['GEN 1:1:0'] }]);
});

Expand Down Expand Up @@ -350,8 +336,8 @@ describe('validateTextAnalysis', () => {
tokenLayer({
tokenAnalyses: [tokenAnalysis('ta-1')],
tokenAnalysisLinks: [
{ ...tokenLink('ta-1', 'approved', 'GEN 1:1:0'), id: 'tl-1' },
{ ...tokenLink('ta-1', 'approved', 'GEN 1:1:5'), id: 'tl-1' },
makeTokenLink('ta-1', 'GEN 1:1:0', { id: 'tl-1' }),
makeTokenLink('ta-1', 'GEN 1:1:5', { id: 'tl-1' }),
],
}),
),
Expand Down Expand Up @@ -395,7 +381,7 @@ describe('validateTextAnalysis', () => {
validateTextAnalysis(
tokenLayer({
tokenAnalyses: [tokenAnalysis('ta-1'), tokenAnalysis('ta-1')],
tokenAnalysisLinks: [tokenLink('ta-1')],
tokenAnalysisLinks: [makeTokenLink('ta-1', 'GEN 1:1:0')],
}),
),
).toEqual([{ kind: 'duplicateAnalysisId', layer: 'token', count: 1, sample: ['ta-1'] }]);
Expand All @@ -406,7 +392,10 @@ describe('validateTextAnalysis', () => {
validateTextAnalysis(
tokenLayer({
tokenAnalyses: [tokenAnalysis('ta-1'), tokenAnalysis('ta-2')],
tokenAnalysisLinks: [tokenLink('ta-1'), tokenLink('ta-2', 'suggested')],
tokenAnalysisLinks: [
makeTokenLink('ta-1', 'GEN 1:1:0'),
makeTokenLink('ta-2', 'GEN 1:1:0', { status: 'suggested' }),
],
}),
),
).toEqual([]);
Expand Down Expand Up @@ -462,14 +451,9 @@ describe('validateTextAnalysis', () => {

describe('hasLinkWithoutId', () => {
it('finds a token link with no id', () => {
expect(
hasLinkWithoutId({
...emptyAnalysis(),
tokenAnalysisLinks: [
{ ...FIXTURE_STAMPS, analysisId: 'ta-1', status: 'approved', token: TOKEN },
],
}),
).toBe(true);
const link: Partial<TokenAnalysisLink> = makeTokenLink('ta-1', 'GEN 1:1:0');
delete link.id;
expect(hasLinkWithoutId({ ...emptyAnalysis(), tokenAnalysisLinks: [link] })).toBe(true);
});

it('finds a segment link with no id', () => {
Expand All @@ -487,7 +471,7 @@ describe('hasLinkWithoutId', () => {
expect(
hasLinkWithoutId(
tokenLayer({
tokenAnalysisLinks: [tokenLink('ta-1')],
tokenAnalysisLinks: [makeTokenLink('ta-1', 'GEN 1:1:0')],
segmentAnalysisLinks: [
{
...FIXTURE_STAMPS,
Expand Down
39 changes: 15 additions & 24 deletions src/__tests__/utils/analysis-book.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type {
SegmentAnalysisLink,
TextAnalysis,
TokenAnalysis,
TokenAnalysisLink,
} from 'interlinearizer';
import {
BOOKLESS_PARTITION,
Expand All @@ -15,7 +14,7 @@ import {
removeBookFromSegmentation,
splitAnalysisByBook,
} from '../../utils/analysis-book';
import { makePhraseLink, FIXTURE_STAMPS } from '../test-helpers';
import { makePhraseLink, makeTokenLink, FIXTURE_STAMPS } from '../test-helpers';

/**
* Creates a minimal {@link TokenAnalysis} payload record fixture, with `surfaceText` defaulting to
Expand All @@ -25,17 +24,6 @@ function mkTokenAnalysis(id: string, surfaceText = id): TokenAnalysis {
return { ...FIXTURE_STAMPS, id, surfaceText };
}

/** Creates a {@link TokenAnalysisLink} joining a token ref to an analysis id. */
function mkTokenLink(analysisId: string, tokenRef: string): TokenAnalysisLink {
return {
id: `${tokenRef}:${analysisId}`,
...FIXTURE_STAMPS,
analysisId,
status: 'approved',
token: { tokenRef, surfaceText: tokenRef },
};
}

/**
* Creates a minimal {@link SegmentAnalysis} payload record fixture, with `surfaceText` defaulting to
* `id`.
Expand Down Expand Up @@ -77,7 +65,7 @@ describe('splitAnalysisByBook', () => {
it('gives each book only its own records', () => {
const analysis: TextAnalysis = {
tokenAnalyses: [mkTokenAnalysis('ta-gen'), mkTokenAnalysis('ta-exo')],
tokenAnalysisLinks: [mkTokenLink('ta-gen', 'GEN 1:1'), mkTokenLink('ta-exo', 'EXO 1:1')],
tokenAnalysisLinks: [makeTokenLink('ta-gen', 'GEN 1:1'), makeTokenLink('ta-exo', 'EXO 1:1')],
segmentAnalyses: [],
segmentAnalysisLinks: [],
phraseAnalyses: [],
Expand Down Expand Up @@ -110,7 +98,7 @@ describe('splitAnalysisByBook', () => {
it('copies a payload shared across books into each book that links it', () => {
const analysis: TextAnalysis = {
tokenAnalyses: [mkTokenAnalysis('shared')],
tokenAnalysisLinks: [mkTokenLink('shared', 'GEN 1:1'), mkTokenLink('shared', 'EXO 1:1')],
tokenAnalysisLinks: [makeTokenLink('shared', 'GEN 1:1'), makeTokenLink('shared', 'EXO 1:1')],
segmentAnalyses: [],
segmentAnalysisLinks: [],
phraseAnalyses: [],
Expand Down Expand Up @@ -144,7 +132,7 @@ describe('splitAnalysisByBook', () => {
it('carries a payload once when two links in the same book share it', () => {
const analysis: TextAnalysis = {
tokenAnalyses: [mkTokenAnalysis('shared')],
tokenAnalysisLinks: [mkTokenLink('shared', 'GEN 1:1'), mkTokenLink('shared', 'GEN 1:2')],
tokenAnalysisLinks: [makeTokenLink('shared', 'GEN 1:1'), makeTokenLink('shared', 'GEN 1:2')],
segmentAnalyses: [],
segmentAnalysisLinks: [],
phraseAnalyses: [],
Expand All @@ -160,7 +148,7 @@ describe('splitAnalysisByBook', () => {
it('keeps a payload no link references out of the linked book’s partition', () => {
const analysis: TextAnalysis = {
tokenAnalyses: [mkTokenAnalysis('orphan')],
tokenAnalysisLinks: [mkTokenLink('linked', 'GEN 1:1')],
tokenAnalysisLinks: [makeTokenLink('linked', 'GEN 1:1')],
segmentAnalyses: [],
segmentAnalysisLinks: [],
phraseAnalyses: [],
Expand All @@ -175,7 +163,7 @@ describe('splitAnalysisByBook', () => {
it('partitions a payload no link references under the bookless key', () => {
const analysis: TextAnalysis = {
tokenAnalyses: [mkTokenAnalysis('orphan')],
tokenAnalysisLinks: [mkTokenLink('linked', 'GEN 1:1')],
tokenAnalysisLinks: [makeTokenLink('linked', 'GEN 1:1')],
segmentAnalyses: [],
segmentAnalysisLinks: [],
phraseAnalyses: [],
Expand Down Expand Up @@ -206,7 +194,7 @@ describe('splitAnalysisByBook', () => {
it('does not file a payload as bookless when another book links it', () => {
const analysis: TextAnalysis = {
tokenAnalyses: [mkTokenAnalysis('shared')],
tokenAnalysisLinks: [mkTokenLink('shared', 'GEN 1:1')],
tokenAnalysisLinks: [makeTokenLink('shared', 'GEN 1:1')],
segmentAnalyses: [],
segmentAnalysisLinks: [],
phraseAnalyses: [],
Expand Down Expand Up @@ -251,15 +239,18 @@ describe('splitAnalysisByBook', () => {
describe('mergeAnalyses', () => {
const genesis: TextAnalysis = {
tokenAnalyses: [mkTokenAnalysis('ta-gen'), mkTokenAnalysis('shared')],
tokenAnalysisLinks: [mkTokenLink('ta-gen', 'GEN 1:1:0'), mkTokenLink('shared', 'GEN 1:1:3')],
tokenAnalysisLinks: [
makeTokenLink('ta-gen', 'GEN 1:1:0'),
makeTokenLink('shared', 'GEN 1:1:3'),
],
segmentAnalyses: [mkSegmentAnalysis('sa-gen')],
segmentAnalysisLinks: [mkSegmentLink('sa-gen', 'GEN 1:1')],
phraseAnalyses: [{ ...FIXTURE_STAMPS, id: 'pa-gen', surfaceText: 'in the' }],
phraseAnalysisLinks: [makePhraseLink('pa-gen', ['GEN 1:1:0', 'GEN 1:1:3'])],
};
const exodus: TextAnalysis = {
tokenAnalyses: [mkTokenAnalysis('shared')],
tokenAnalysisLinks: [mkTokenLink('shared', 'EXO 1:1:0')],
tokenAnalysisLinks: [makeTokenLink('shared', 'EXO 1:1:0')],
segmentAnalyses: [mkSegmentAnalysis('sa-exo')],
segmentAnalysisLinks: [mkSegmentLink('sa-exo', 'EXO 1:1')],
phraseAnalyses: [],
Expand Down Expand Up @@ -321,10 +312,10 @@ describe('removeBookFromAnalysis', () => {
mkTokenAnalysis('tok-orphan'),
],
tokenAnalysisLinks: [
mkTokenLink('tok-gen', 'GEN 1:1:0'),
mkTokenLink('tok-exo', 'EXO 2:2:0'),
makeTokenLink('tok-gen', 'GEN 1:1:0'),
makeTokenLink('tok-exo', 'EXO 2:2:0'),
// Only link referencing tok-orphan is a GEN link → removing GEN orphans the payload.
mkTokenLink('tok-orphan', 'GEN 3:3:0'),
makeTokenLink('tok-orphan', 'GEN 3:3:0'),
],
segmentAnalyses: [mkSegmentAnalysis('seg-gen'), mkSegmentAnalysis('seg-exo')],
segmentAnalysisLinks: [
Expand Down
Loading
Loading