mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-11 04:46:07 -08:00
feat(plugins): add JavaScript plugin system with overlay panel (#303)
This commit is contained in:
1 parent
90fc7cf940
commit
a9a4c4f083
110 files changed
+5897
-344
No files matched your search
+39
-21
@@ -28,6 +28,8 @@ type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
|
||||
export type E2eSessionOptions = {
|
||||
/** Deep-merged over the harness config, for scenarios that need other settings. */
|
||||
config?: JsonObject;
|
||||
/** Files written before launch, keyed by path relative to the config dir (e.g. plugins). */
|
||||
files?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type AppTarget = {
|
||||
@@ -57,6 +59,8 @@ export type E2eSession = {
|
||||
mainProcess: () => Promise<CdpPage>;
|
||||
/** Runs a CLI command (e.g. `--mine-sentence`) against the running instance. */
|
||||
runAppCommand: (...args: string[]) => Promise<void>;
|
||||
/** After loading a file: waits for the app's autoplay resume, then leaves mpv paused. */
|
||||
settleAfterResume: () => Promise<void>;
|
||||
dispose: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -155,6 +159,30 @@ async function openPage(cdpPort: number, urlPart: string): Promise<CdpPage> {
|
||||
return page;
|
||||
}
|
||||
|
||||
// mpv starts paused and the app resumes each newly loaded file once the overlay
|
||||
// and tokenizer are ready, so that resume marks the file as ready. Scenarios
|
||||
// expect a paused player, and the app retries its resume for a moment, so keep
|
||||
// pausing until the pause has held for longer than that retry window.
|
||||
async function settleAfterResume(mpv: MpvClient): Promise<void> {
|
||||
await waitUntil(async () => (await mpv.command('get_property', 'pause')) === false, {
|
||||
description: 'SubMiner to resume playback once the file is ready',
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
let pausedSince: number | null = null;
|
||||
await waitUntil(
|
||||
async () => {
|
||||
if ((await mpv.command('get_property', 'pause')) !== true) {
|
||||
pausedSince = null;
|
||||
await mpv.command('set_property', 'pause', true);
|
||||
return false;
|
||||
}
|
||||
pausedSince ??= Date.now();
|
||||
return Date.now() - pausedSince >= 1_500;
|
||||
},
|
||||
{ description: 'playback to stay paused after the resume', timeoutMs: 40_000 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function startE2eSession(options: E2eSessionOptions = {}): Promise<E2eSession> {
|
||||
if (!fs.existsSync(path.join(repoRoot, 'dist', 'main-entry.js'))) {
|
||||
throw new Error('dist/ is missing. Run `bun run build` before the e2e harness.');
|
||||
@@ -193,6 +221,15 @@ export async function startE2eSession(options: E2eSessionOptions = {}): Promise<
|
||||
|
||||
const config = mergeConfig(buildConfig(mpvSocketPath, anki.url), options.config ?? {});
|
||||
fs.writeFileSync(path.join(userDataDir, 'config.jsonc'), JSON.stringify(config, null, 2));
|
||||
for (const [relativePath, content] of Object.entries(options.files ?? {})) {
|
||||
const filePath = path.resolve(userDataDir, relativePath);
|
||||
const inside = path.relative(userDataDir, filePath);
|
||||
if (!inside || inside.startsWith('..') || path.isAbsolute(inside)) {
|
||||
throw new Error(`E2E file path must stay inside the config dir: ${relativePath}`);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
|
||||
const env: EnvDelta = {
|
||||
set: {
|
||||
@@ -287,27 +324,7 @@ export async function startE2eSession(options: E2eSessionOptions = {}): Promise<
|
||||
teardown.push(mpv.close);
|
||||
|
||||
const app = await launchApp(['--start']);
|
||||
// mpv starts paused and the app resumes it once the overlay and tokenizer
|
||||
// are ready, so that resume marks the end of startup.
|
||||
await waitUntil(async () => (await mpv.command('get_property', 'pause')) === false, {
|
||||
description: 'SubMiner to resume playback after overlay startup',
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
// Hand scenarios a paused player. The app retries its resume for a moment,
|
||||
// so keep pausing until the pause has held for longer than that retry window.
|
||||
let pausedSince: number | null = null;
|
||||
await waitUntil(
|
||||
async () => {
|
||||
if ((await mpv.command('get_property', 'pause')) !== true) {
|
||||
pausedSince = null;
|
||||
await mpv.command('set_property', 'pause', true);
|
||||
return false;
|
||||
}
|
||||
pausedSince ??= Date.now();
|
||||
return Date.now() - pausedSince >= 1_500;
|
||||
},
|
||||
{ description: 'playback to stay paused after startup', timeoutMs: 40_000 },
|
||||
);
|
||||
await settleAfterResume(mpv);
|
||||
const pages: CdpPage[] = [];
|
||||
teardown.push(() => pages.forEach((page) => page.close()));
|
||||
|
||||
@@ -331,6 +348,7 @@ export async function startE2eSession(options: E2eSessionOptions = {}): Promise<
|
||||
return page;
|
||||
},
|
||||
runAppCommand: (...args) => runAppCommand(target, args),
|
||||
settleAfterResume: () => settleAfterResume(mpv),
|
||||
dispose,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
Reference in new issue
Block a user