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
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ describe('Liveness Machine', () => {
await flushPromises(); // flashFreshnessColors
}

async function transitionToGetLivenessResult(service: LivenessInterpreter) {
await transitionToUploading(service);
await flushPromises(); // stopVideo
service.send({ type: 'DISCONNECT_EVENT' });
jest.advanceTimersToNextTimer(); // waitForDisconnect
await flushPromises(); // getLivenessResult - this triggers the getLiveness action
}

beforeEach(() => {
Object.defineProperty(global.navigator, 'mediaDevices', {
value: mockNavigatorMediaDevices,
Expand Down Expand Up @@ -1124,5 +1132,236 @@ describe('Liveness Machine', () => {
expect(mockedHelpers.getTrackDimensions).toHaveBeenCalled();
expect(service.state.context.livenessStreamProvider).toBeDefined();
});

// Task 7: Tests for video recording duration and unique timestamps
describe('Video recording duration and timestamps', () => {
it('should verify video recording duration matches expected duration', async () => {
const mockStartTime = Date.now();
const mockEndTime = mockStartTime + 8000; // 8 seconds

// Mock the timestamps
jest
.spyOn(Date, 'now')
.mockReturnValueOnce(mockStartTime) // recordingStartTimestampActual
.mockReturnValueOnce(mockEndTime); // freshnessColorEndTimestamp

// Mock getClientSessionInfoEvents to return color signals
const mockColorSignals = [
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime,
PreviousColorStartTimestamp: 0,
CurrentColor: { RGB: [0, 0, 0] },
},
},
},
},
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime + 1000,
PreviousColorStartTimestamp: mockStartTime,
CurrentColor: { RGB: [255, 255, 255] },
},
},
},
},
];

(
mockStreamRecorder.getClientSessionInfoEvents as jest.Mock
).mockReturnValue(mockColorSignals);

await transitionToUploading(service);

// Verify that the recording duration calculation would be correct
const expectedDuration = mockEndTime - mockStartTime;
expect(expectedDuration).toBe(8000);
});

it('should verify all colors are captured with unique timestamps', async () => {
const mockStartTime = Date.now();

// Create mock color signals with unique, increasing timestamps
const mockColorSignals = [
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime,
PreviousColorStartTimestamp: 0,
CurrentColor: { RGB: [0, 0, 0] },
},
},
},
},
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime + 400,
PreviousColorStartTimestamp: mockStartTime,
CurrentColor: { RGB: [255, 255, 255] },
},
},
},
},
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime + 800,
PreviousColorStartTimestamp: mockStartTime + 400,
CurrentColor: { RGB: [255, 0, 0] },
},
},
},
},
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime + 1200,
PreviousColorStartTimestamp: mockStartTime + 800,
CurrentColor: { RGB: [255, 255, 0] },
},
},
},
},
];

(
mockStreamRecorder.getClientSessionInfoEvents as jest.Mock
).mockReturnValue(mockColorSignals);

await transitionToUploading(service);

// Extract the color signals
const colorSignals = mockStreamRecorder
.getClientSessionInfoEvents()
.map((info: any) => {
return info.Challenge?.FaceMovementAndLightChallenge
?.ColorDisplayed;
})
.filter(Boolean);

// Verify all colors have unique timestamps
const timestamps = colorSignals.map(
(signal: any) => signal.CurrentColorStartTimestamp
);
const uniqueTimestamps = new Set(timestamps);

expect(timestamps.length).toBe(4);
expect(uniqueTimestamps.size).toBe(4);
expect(timestamps.length).toBe(uniqueTimestamps.size);

// Verify timestamps are monotonically increasing
for (let i = 1; i < timestamps.length; i++) {
expect(timestamps[i]).toBeGreaterThan(timestamps[i - 1]);
}
});

it('should ensure timestamps are monotonically increasing', async () => {
const mockStartTime = Date.now();

// Create mock color signals with properly increasing timestamps
const mockColorSignals = [
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime,
PreviousColorStartTimestamp: 0,
CurrentColor: { RGB: [0, 0, 0] },
},
},
},
},
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime + 100, // Properly incremented
PreviousColorStartTimestamp: mockStartTime,
CurrentColor: { RGB: [255, 255, 255] },
},
},
},
},
];

(
mockStreamRecorder.getClientSessionInfoEvents as jest.Mock
).mockReturnValue(mockColorSignals);

await transitionToGetLivenessResult(service);

// Verify timestamps are monotonically increasing
const timestamps = mockColorSignals.map(
(signal) =>
signal.Challenge.FaceMovementAndLightChallenge.ColorDisplayed
.CurrentColorStartTimestamp
);
for (let i = 1; i < timestamps.length; i++) {
expect(timestamps[i]).toBeGreaterThan(timestamps[i - 1]);
}
});

it('should complete liveness check with valid color sequence data', async () => {
const mockStartTime = Date.now();
const mockColorSignals = [
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime,
PreviousColorStartTimestamp: 0,
CurrentColor: { RGB: [0, 0, 0] },
},
},
},
},
{
Challenge: {
FaceMovementAndLightChallenge: {
ColorDisplayed: {
CurrentColorStartTimestamp: mockStartTime + 100,
PreviousColorStartTimestamp: mockStartTime,
CurrentColor: { RGB: [255, 255, 255] },
},
},
},
},
];

(
mockStreamRecorder.getClientSessionInfoEvents as jest.Mock
).mockReturnValue(mockColorSignals);

await transitionToGetLivenessResult(service);

// Verify the liveness check completed successfully
expect(
mockStreamRecorder.getClientSessionInfoEvents
).toHaveBeenCalled();

// Verify color signals were captured
const colorSignals = mockStreamRecorder.getClientSessionInfoEvents();
expect(colorSignals.length).toBeGreaterThan(0);

// Verify each signal has the expected structure
colorSignals.forEach((signal: any) => {
expect(
signal.Challenge.FaceMovementAndLightChallenge.ColorDisplayed
).toHaveProperty('CurrentColorStartTimestamp');
expect(
signal.Challenge.FaceMovementAndLightChallenge.ColorDisplayed
).toHaveProperty('CurrentColor');
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
createColorDisplayEvent,
createSessionEndEvent,
getTrackDimensions,
validateVideoDuration,
} from '../utils';

import {
Expand Down Expand Up @@ -600,9 +601,13 @@ export const livenessMachine = createMachine<LivenessContext, LivenessEvent>(
return { ...context.videoAssociatedParams };
},
}),
stopRecording: () => {
freshnessColorEndTimestamp = Date.now();
},
stopRecording: assign({
videoAssociatedParams: (context) => {
// Set the end timestamp immediately when entering success state
freshnessColorEndTimestamp = Date.now();
return { ...context.videoAssociatedParams };
},
}),
updateFaceMatchBeforeStartDetails: assign({
faceMatchStateBeforeStart: (_, event) =>
event.data!.faceMatchState as FaceMatchState,
Expand Down Expand Up @@ -1288,6 +1293,9 @@ export const livenessMachine = createMachine<LivenessContext, LivenessEvent>(
.getTracks()[0]
.getSettings();

// Capture the timestamp BEFORE stopping to get accurate recording end time
recordingEndTimestamp = Date.now();

// if not awaited, `getRecordingEndTimestamp` will throw
await livenessStreamProvider!.stopRecording();

Expand All @@ -1308,8 +1316,6 @@ export const livenessMachine = createMachine<LivenessContext, LivenessEvent>(
}),
});

recordingEndTimestamp = Date.now();

livenessStreamProvider!.dispatchStreamEvent({ type: 'streamStop' });
},
// eslint-disable-next-line @typescript-eslint/require-await
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,36 +119,41 @@ export class ColorSequenceDisplay {
onSequenceStart();
}

const sequenceStartTime = Date.now();
const currentTime = Date.now();

let timeSinceLastColorStageChange =
sequenceStartTime - this.#lastColorStageChangeTimestamp;
currentTime - this.#lastColorStageChangeTimestamp;

// Send a colorStart time only for the first tick of the first color
if (this.#isFirstTick) {
this.#lastColorStageChangeTimestamp = Date.now();
const firstSequenceTimestamp = Date.now();
this.#lastColorStageChangeTimestamp = firstSequenceTimestamp;
this.#isFirstTick = false;

// initial sequence change
// initial sequence change - capture timestamp at the exact moment
if (isFunction(onSequenceChange)) {
onSequenceChange({
prevSequenceColor: this.#previousSequence.color,
sequenceColor: this.#sequence.color,
sequenceIndex: this.#sequenceIndex,
sequenceStartTime,
sequenceStartTime: firstSequenceTimestamp,
});
}
}

// Every 10 ms tick we will check if the threshold for flat or scrolling, if so we will try to go to the next stage
if (
(this.#isFlatStage() &&
timeSinceLastColorStageChange >= this.#sequence.flatDisplayDuration) ||
(this.#isScrollingStage() &&
timeSinceLastColorStageChange >= this.#sequence.downscrollDuration)
) {
this.#handleSequenceChange({ sequenceStartTime, onSequenceChange });
timeSinceLastColorStageChange = 0;
// Skip transition check on first tick to ensure unique timestamps
// The next tick will handle any immediate transitions
} else {
// Every 10 ms tick we will check if the threshold for flat or scrolling, if so we will try to go to the next stage
if (
(this.#isFlatStage() &&
timeSinceLastColorStageChange >=
this.#sequence.flatDisplayDuration) ||
(this.#isScrollingStage() &&
timeSinceLastColorStageChange >= this.#sequence.downscrollDuration)
) {
this.#handleSequenceChange({ onSequenceChange });
timeSinceLastColorStageChange = 0;
}
}

const hasRemainingSequences =
Expand Down Expand Up @@ -184,10 +189,8 @@ export class ColorSequenceDisplay {
// SCROLL - prev = 1, curr = 2
// SCROLL - prev = 2, curr = 3
#handleSequenceChange({
sequenceStartTime,
onSequenceChange,
}: {
sequenceStartTime: number;
onSequenceChange?: OnSequenceChange;
}) {
this.#previousSequence = this.#sequence;
Expand All @@ -207,15 +210,17 @@ export class ColorSequenceDisplay {

this.#sequence = this.#colorSequences[this.#sequenceIndex];

this.#lastColorStageChangeTimestamp = Date.now();
// Capture the actual timestamp when this sequence change occurs
const actualSequenceStartTime = Date.now();
this.#lastColorStageChangeTimestamp = actualSequenceStartTime;

if (this.#sequence) {
if (isFunction(onSequenceChange)) {
onSequenceChange({
prevSequenceColor: this.#previousSequence.color,
sequenceColor: this.#sequence.color,
sequenceIndex: this.#sequenceIndex,
sequenceStartTime: sequenceStartTime,
sequenceStartTime: actualSequenceStartTime,
});
}
}
Expand Down
Loading