// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {RecorderApp} from '../pages/recorder-app.js';
import {
useRecordingDataManager,
} from './lit/context.js';
import {TextToken, Transcription} from './soda/soda.js';
import {navigateTo} from './state/route.js';
import {
settings,
SpeakerLabelEnableState,
SummaryEnableState,
TranscriptionEnableState,
} from './state/settings.js';
import {assertExists} from './utils/assert.js';
interface ConfigForTest {
includeSystemAudio: boolean;
showOnboardingDialog: boolean;
speakerLabelForceEnabled: boolean;
summaryForceEnabled: boolean;
transcriptionForceEnabled: boolean;
}
interface RecordingDataForTest {
audio: string;
durationMs: number;
title: string;
powers: number[];
textTokens?: TextToken[];
}
function base64ToBlob(data: string): Blob {
const mimeType = 'audio/webm;codecs=opus';
const dataPart = assertExists(data.split(',')[1], 'Invalid audio data');
const byteCharacters = atob(dataPart);
const n = byteCharacters.length;
const byteArray = new Uint8Array(n);
for (let i = 0; i < n; i++) {
byteArray[i] = byteCharacters.charCodeAt(i);
}
return new Blob([byteArray], {type: mimeType});
}
/**
* TestHelper is a helper class used by Tast test only.
*/
export class TestHelper {
/**
* Redirect to the main page. It is called when the set up is done and ready
* to start the test.
*/
static goToMainPage(): void {
navigateTo('/');
}
/**
* Removes the local settings and recordings.
*/
static async removeCacheData(): Promise<void> {
localStorage.clear();
await useRecordingDataManager().clear();
}
/**
* Configures local settings requested for the test.
*
* @param config Configuration sent from the test.
*/
static configureSettingsForTest(config: ConfigForTest): void {
settings.mutate((s) => {
// Pre-set the mic option to include/exclude system audio.
s.includeSystemAudio = config.includeSystemAudio;
// Skip onboarding dialog at the start of the test.
if (!config.showOnboardingDialog) {
s.onboardingDone = true;
}
// Force enable models for testing.
if (config.speakerLabelForceEnabled) {
s.speakerLabelEnabled = SpeakerLabelEnableState.ENABLED;
}
if (config.summaryForceEnabled) {
s.summaryEnabled = SummaryEnableState.ENABLED;
}
if (config.transcriptionForceEnabled) {
s.transcriptionEnabled = TranscriptionEnableState.ENABLED;
}
});
}
/**
* Imports the recordings sent from Tast side.
*
* @param recordings A list of recordings.
*/
static async importRecordings(
recordings: RecordingDataForTest[],
): Promise<void> {
for (const data of recordings) {
const {audio, durationMs, powers, title, textTokens: tokens} = data;
const blob = base64ToBlob(audio);
const params = {
title: title,
durationMs: durationMs,
recordedAt: Date.now(),
powers: powers,
transcription: tokens !== undefined ? new Transcription(tokens) : null,
};
await useRecordingDataManager().createRecording(params, blob);
}
}
// UI-related functions.
/**
* Returns the UI from the given key, throws an error if not exists.
*
* @param key UI key listed in `UI_COMPONENTS` object.
* @return The resolved UI element.
*/
static resolveComponent(key: ComponentKey): Element {
return UI_COMPONENTS[key]();
}
/**
* Returns the number of the recording files in the main page.
*
* @return Number of recording files in the main Page.
*/
static getRecordingFileCount(): number {
return app()
.mainPageForTest.recordingFileListForTest.recordingFileCountForTest();
}
/**
* Returns the title suggestion given the index.
*
* @param index Zero-based index.
* @return An element of the n-th title suggestion.
*/
static getNthSuggestedTitle(index: number): Element {
return app()
.playbackPageForTest.recordingTitleForTest.titleSuggestionForTest
.nthSuggestedTitleForTest(index);
}
/**
* Returns the summary shown in the playback page.
*
* @return A string containing the recording summary, may contains `\n`.
*/
static getSummaryContent(): string {
return app()
.playbackPageForTest.summarizationViewForTest.getSummaryContentForTest();
}
}
/**
* Returns the `RecorderApp` queried from the document.
*
* @return `RecorderApp` object.
*/
function app(): RecorderApp {
return assertExists(document.querySelector('recorder-app'));
}
// TODO: b/361015174 - Simplify the approach to access UI components.
const UI_COMPONENTS = {
firstSuggestedTitle: () =>
app()
.playbackPageForTest.recordingTitleForTest.titleSuggestionForTest
.firstSuggestedTitleForTest,
firstRecordingCard: () => app()
.mainPageForTest.recordingFileListForTest
.firstRecordingForTest.recordingCardForTest,
mainPage: () => app().mainPageForTest,
playbackBackButton: () => app().playbackPageForTest.backButtonForTest,
playbackPage: () => app().playbackPageForTest,
playbackPauseButton: () => app().playbackPageForTest.pauseButtonForTest,
playbackRecordingTitle: () => app().playbackPageForTest.recordingTitleForTest,
playbackTranscriptionToggleButton: () =>
app().playbackPageForTest.transcriptionToggleButtonForTest,
recordPage: () => app().recordPageForTest,
renameTitleText: () =>
app().playbackPageForTest.recordingTitleForTest.renameContainerForTest,
suggestTitleButton: () =>
app().playbackPageForTest.recordingTitleForTest.suggestTitleButtonForTest,
summaryContainer: () =>
app().playbackPageForTest.summarizationViewForTest.summaryContainerForTest,
startRecordingButton: () => app().mainPageForTest.startRecordingButtonForTest,
stopRecordingButton: () => app().recordPageForTest.stopRecordingButtonForTest,
toggleSummaryButton: () =>
app()
.playbackPageForTest.summarizationViewForTest.toggleSummaryButtonForTest,
};
type ComponentKey = keyof typeof UI_COMPONENTS;