Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

let captured: { hasPermission?: boolean; curRole?: string } = {};
let captured: {
hasPermission?: boolean;
curRole?: string;
phraseRegions?: unknown;
} = {};

/** Per-test knobs for the phrase-segment path (TT-7666). */
const phrase = {
isPhraseArtifact: false,
slug: 'vernacular',
regions: [] as unknown[],
/** What `related()` answers for a row's sourceMedia. */
sourceMedia: undefined as string | undefined,
};

const linkedSharedResource = {
id: 'sr1',
Expand All @@ -18,7 +31,10 @@ const passageDetailCtx = {
orgWorkflowSteps: [
{
id: 'step-transcribe',
attributes: { sequencenum: 1, tool: '{"tool":"transcribe","settings":{}}' },
attributes: {
sequencenum: 1,
tool: '{"tool":"transcribe","settings":{}}',
},
},
],
setStepComplete: jest.fn(),
Expand All @@ -30,7 +46,10 @@ const passageDetailCtx = {
sharedResource: undefined as unknown,
};

jest.mock('../../context/usePassageDetailContext', () => () => passageDetailCtx);
jest.mock(
'../../context/usePassageDetailContext',
() => () => passageDetailCtx
);

jest.mock('../../context/PassageDetailContext', () => ({
PassageDetailContext: React.createContext({ setState: jest.fn() }),
Expand All @@ -39,9 +58,11 @@ jest.mock('../../context/PassageDetailContext', () => ({
jest.mock('../../context/TranscriberContext', () => ({
TranscriberProvider: (props: {
curRole?: string;
phraseRegions?: unknown;
children?: React.ReactNode;
}) => {
captured.curRole = props.curRole;
captured.phraseRegions = props.phraseRegions;
return <>{props.children}</>;
},
}));
Expand Down Expand Up @@ -83,20 +104,20 @@ jest.mock('../../crud', () => ({
jest.mock('../../crud/useArtifactType', () => ({
useArtifactType: () => ({
localizedArtifactTypeFromId: () => 'bt',
slugFromId: () => 'vernacular',
slugFromId: () => phrase.slug,
}),
}));

jest.mock('../../crud/artifactTypeSlug', () => ({
ArtifactTypeSlug: { CarefulSpeech: 'carefulspeech' },
artifactStampsStepLanguage: () => false,
isPhraseSegmentArtifact: () => false,
isPhraseSegmentArtifact: () => phrase.isPhraseArtifact,
}));

jest.mock('../../crud/related', () => ({
related: jest.fn(),
related: () => phrase.sourceMedia,
__esModule: true,
default: jest.fn(),
default: () => phrase.sourceMedia,
}));

jest.mock('../../utils/useStepPermission', () => ({
Expand All @@ -106,7 +127,13 @@ jest.mock('../../utils/useStepPermission', () => ({
}));

jest.mock('../../hoc/useOrbitData', () => ({
useOrbitData: () => [],
useOrbitData: () => [
{
id: 'mf1',
type: 'mediafile',
attributes: { versionNumber: 1, segments: '[]' },
},
],
}));

jest.mock('../../context/UnsavedContext', () => {
Expand All @@ -130,12 +157,12 @@ jest.mock('react-redux', () => ({

jest.mock('../../utils/namedSegments', () => ({
getSegments: () => '{}',
getSortedRegions: () => [],
getSortedRegions: () => phrase.regions,
NamedRegions: { Clause: 'clause', BackTranslation: 'bt' },
}));

jest.mock('./carefulSpeech/carefulSpeechBoundary', () => ({
hasPhraseRegions: () => false,
hasPhraseRegions: () => phrase.regions.length > 0,
}));

jest.mock('./carefulSpeech/matchesGuidedOutputRow', () => ({
Expand All @@ -152,12 +179,19 @@ jest.mock('./boldClause/StepMessage', () => () => null);

import { PassageDetailTranscribe } from './PassageDetailTranscribe';

const resetKnobs = () => {
captured = {};
passageDetailCtx.sharedResource = undefined;
passageDetailCtx.mediafileId = 'mf1';
passageDetailCtx.rowData = [];
phrase.isPhraseArtifact = false;
phrase.slug = 'vernacular';
phrase.regions = [];
phrase.sourceMedia = undefined;
};

describe('PassageDetailTranscribe linked note (TT-5873)', () => {
beforeEach(() => {
captured = {};
passageDetailCtx.sharedResource = undefined;
passageDetailCtx.mediafileId = 'mf1';
});
beforeEach(resetKnobs);

it('keeps transcribe permission on the source note', () => {
render(<PassageDetailTranscribe width={400} artifactTypeId={null} />);
Expand All @@ -175,3 +209,38 @@ describe('PassageDetailTranscribe linked note (TT-5873)', () => {
expect(captured.curRole).toBe('view');
});
});

/**
* TT-7666 - the task list is built from every take attached to the vernacular,
* so the takes left behind by a segment-boundary adjustment showed up beside
* the ones recorded after it: two segments, four tasks to transcribe. Which
* takes are still current is decided against the segment boundaries the step is
* reading, so the provider has to be told what they are.
*/
describe('PassageDetailTranscribe phrase takes (TT-7666)', () => {
const clauseRegions = [
{ start: 0, end: 6, label: '' },
{ start: 6, end: 10, label: '' },
];

beforeEach(() => {
resetKnobs();
phrase.sourceMedia = 'mf1';
passageDetailCtx.rowData = [
{ artifactType: 'bt', mediafile: { id: 'take1', type: 'mediafile' } },
] as never;
});

it('hands the current segment boundaries to the transcriber provider', () => {
phrase.isPhraseArtifact = true;
phrase.slug = 'carefulspeech';
phrase.regions = clauseRegions;
render(<PassageDetailTranscribe width={400} artifactTypeId={'art1'} />);
expect(captured.phraseRegions).toEqual(clauseRegions);
});

it('leaves a non-phrase artifact unscoped', () => {
render(<PassageDetailTranscribe width={400} artifactTypeId={'art1'} />);
expect(captured.phraseRegions ?? []).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export function PassageDetailTranscribe({ width, artifactTypeId }: IProps) {
artifactTypeId={artifactTypeId}
curRole={curRole as string}
stepLanguageBcp47={stepLanguageBcp47}
phraseRegions={phraseRegions}
>
<Grid
container
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
matchesGuidedOutputRow,
pickLatestGuidedOutputRow,
} from './matchesGuidedOutputRow';

const REGION_TOLERANCE = 0.05;
import { parseTakeSourceRegion, regionsMatch } from '../../../crud/phraseTakes';

function isEmptySourceSegments(seg: string | undefined): boolean {
if (!seg) return true;
Expand All @@ -20,19 +19,6 @@ function isEmptySourceSegments(seg: string | undefined): boolean {
}
}

function parseStoredRegion(seg: string | undefined): IRegion | undefined {
if (!seg) return undefined;
try {
const parsed = JSON.parse(seg) as IRegion;
if (parsed?.start !== undefined && parsed?.end !== undefined) {
return parsed;
}
} catch {
return undefined;
}
return undefined;
}

function regionMatchesClause(
storedSeg: string | undefined,
clauseRegion: IRegion,
Expand All @@ -45,13 +31,8 @@ function regionMatchesClause(
) {
return true;
}
const stored = parseStoredRegion(storedSeg);
if (stored) {
return (
Math.abs(stored.start - clauseRegion.start) < REGION_TOLERANCE &&
Math.abs(stored.end - clauseRegion.end) < REGION_TOLERANCE
);
}
const stored = parseTakeSourceRegion(storedSeg);
if (stored) return regionsMatch(stored, clauseRegion);
return prettySegment(storedSeg).trim() === prettySegment(clauseRegion).trim();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { related } from '../../../crud/related';
import { IRow } from '../../../context/PassageDetailContext';
import { mediaMatchesStepLanguage } from '../../../utils/mediaLanguage';
import { compareTakesNewestFirst } from '../../../crud/phraseTakes';

// Language-field helpers live in utils/mediaLanguage so context-layer callers
// don't have to import from this component subtree. Re-exported here because
Expand Down Expand Up @@ -47,12 +48,9 @@ export function matchesGuidedOutputRow(
export function pickLatestGuidedOutputRow(matches: IRow[]): IRow | undefined {
if (matches.length === 0) return undefined;
if (matches.length === 1) return matches[0];
return [...matches].sort((a, b) => {
const da = a.mediafile?.attributes?.dateCreated ?? '';
const db = b.mediafile?.attributes?.dateCreated ?? '';
if (da !== db) return db.localeCompare(da);
return (b.mediafile?.id ?? '').localeCompare(a.mediafile?.id ?? '');
})[0];
return [...matches].sort((a, b) =>
compareTakesNewestFirst(a.mediafile, b.mediafile)
)[0];
}

/** Named-region key for Phrase BT segment boundaries for a language. */
Expand Down
21 changes: 19 additions & 2 deletions src/renderer/src/context/TranscriberContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
} from '../crud';
import { mediaFileName } from '../crud/media';
import { mediaMatchesStepLanguage } from '../utils/mediaLanguage';
import { selectCurrentPhraseTakes } from '../crud/phraseTakes';
import { IRegion } from '../crud/useWavesurferRegions';
import StickyRedirect from '../components/StickyRedirect';
import { useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
Expand Down Expand Up @@ -133,9 +135,15 @@ interface IProps {
curRole?: string;
/** Step language. When set (and not `und`), only media tagged with it become tasks. */
stepLanguageBcp47?: string;
/**
* Phrase-segment boundaries the step is reading. When given, a segment
* contributes its newest take and the takes left behind by a boundary
* adjustment are not tasks (TT-7666).
*/
phraseRegions?: IRegion[];
}
const TranscriberProvider = (props: IProps) => {
const { artifactTypeId, curRole, stepLanguageBcp47 } = props;
const { artifactTypeId, curRole, stepLanguageBcp47, phraseRegions } = props;
const [isDetail] = useState(artifactTypeId !== undefined);
const passages = useOrbitData<Passage[]>('passage');
const sections = useOrbitData<Section[]>('section');
Expand Down Expand Up @@ -200,10 +208,19 @@ const TranscriberProvider = (props: IProps) => {
return;
}
m = m.filter((mf) => mediaMatchesStepLanguage(mf, stepLanguageBcp47));
m = selectCurrentPhraseTakes(m, phraseRegions ?? []);
setPlanMedia(m);
planMediaRef.current = m;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mediafiles, devPlan, artifactId, stepLanguageBcp47, pasId, memory]);
}, [
mediafiles,
devPlan,
artifactId,
stepLanguageBcp47,
phraseRegions,
pasId,
memory,
]);

const setRows = (rowData: IRowData[]) => {
setState((state: ICtxState) => {
Expand Down
Loading
Loading