fix(workspace): defer embedded mpv palette support check

This commit is contained in:
4gray committed 2026-06-28 02:01:38 +02:00
1 parent f7ad17298f
commit d7e7a8abda
6 files changed
+108 -22

No files matched your search

@@ -50,6 +50,14 @@ describe('WorkspacePlayerCommandsContributor', () => {
supportsManagedExternalPlayers: boolean;
supportsEmbeddedMpv: boolean;
};
let electronStub:
| {
getEmbeddedMpvSupport: jest.Mock<
Promise<{ supported: boolean }>,
[]
>;
}
| undefined;
let translate: { instant: jest.Mock; onLangChange: ReturnType<typeof of> };
function bootstrap(options: {
@@ -72,7 +80,7 @@ describe('WorkspacePlayerCommandsContributor', () => {
supportsEmbeddedMpv: options.supportsEmbeddedMpv ?? false,
};
const electronStub = runtime.supportsEmbeddedMpv
electronStub = runtime.supportsEmbeddedMpv
? {
getEmbeddedMpvSupport: jest.fn().mockResolvedValue(
options.embeddedMpvSupportResult ?? {
@@ -153,15 +161,23 @@ describe('WorkspacePlayerCommandsContributor', () => {
expect(resolveBoolean(embedded?.visible)).toBe(false);
});
it('shows embedded MPV once support resolves to supported', async () => {
it('does not verify embedded MPV support during contributor bootstrap', () => {
bootstrap({
supportsManagedExternalPlayers: true,
supportsEmbeddedMpv: true,
});
expect(electronStub?.getEmbeddedMpvSupport).not.toHaveBeenCalled();
});
it('shows embedded MPV once support resolves to supported', async () => {
const contributor = bootstrap({
supportsManagedExternalPlayers: true,
supportsEmbeddedMpv: true,
embeddedMpvSupportResult: { supported: true },
});
await Promise.resolve();
await Promise.resolve();
await contributor.ensureEmbeddedMpvSupportLoaded();
const embedded = getRegistered(viewCommands).find(
(c) => c.id === 'switch-player-embedded-mpv'
@@ -170,14 +186,13 @@ describe('WorkspacePlayerCommandsContributor', () => {
});
it('keeps embedded MPV hidden when support resolves to unsupported', async () => {
bootstrap({
const contributor = bootstrap({
supportsManagedExternalPlayers: true,
supportsEmbeddedMpv: true,
embeddedMpvSupportResult: { supported: false },
});
await Promise.resolve();
await Promise.resolve();
await contributor.ensureEmbeddedMpvSupportLoaded();
const embedded = getRegistered(viewCommands).find(
(c) => c.id === 'switch-player-embedded-mpv'
@@ -86,14 +86,14 @@ export class WorkspacePlayerCommandsContributor {
private readonly destroyRef = inject(DestroyRef);
private readonly runtime = inject(RuntimeCapabilitiesService);
private readonly embeddedMpvSupported = signal(false);
private embeddedMpvSupportChecked = false;
private embeddedMpvSupportLoad: Promise<void> | null = null;
constructor() {
const unregisters = PLAYER_COMMAND_DEFS.map((def) =>
this.viewCommands.registerCommand(this.toContribution(def))
);
void this.loadEmbeddedMpvSupport();
this.destroyRef.onDestroy(() => {
for (const unregister of unregisters) {
unregister();
@@ -101,23 +101,36 @@ export class WorkspacePlayerCommandsContributor {
});
}
private async loadEmbeddedMpvSupport(): Promise<void> {
ensureEmbeddedMpvSupportLoaded(): Promise<void> | undefined {
if (this.embeddedMpvSupportChecked) {
return undefined;
}
if (
!this.runtime.supportsEmbeddedMpv ||
typeof window === 'undefined' ||
!window.electron?.getEmbeddedMpvSupport
) {
return;
this.embeddedMpvSupportChecked = true;
return undefined;
}
this.embeddedMpvSupportLoad ??= this.loadEmbeddedMpvSupport();
return this.embeddedMpvSupportLoad;
}
private async loadEmbeddedMpvSupport(): Promise<void> {
try {
const support = await window.electron.getEmbeddedMpvSupport();
const support = await window.electron?.getEmbeddedMpvSupport?.();
this.embeddedMpvSupported.set(!!support?.supported);
} catch (error) {
console.warn(
'Failed to verify embedded MPV support for the command palette.',
error
);
} finally {
this.embeddedMpvSupportChecked = true;
this.embeddedMpvSupportLoad = null;
}
}
@@ -20,8 +20,7 @@ export class WorkspaceShellCommandPaletteService {
private readonly viewCommands = inject(WorkspaceViewCommandService);
private readonly recentCommands = inject(RecentCommandsService);
private readonly destroyRef = inject(DestroyRef);
// Eager construction registers the player-switch commands via WorkspaceViewCommandService.
private readonly _playerCommandsBootstrap = inject(
private readonly playerCommands = inject(
WorkspacePlayerCommandsContributor
);
@@ -29,6 +28,7 @@ export class WorkspaceShellCommandPaletteService {
WorkspaceCommandPaletteComponent,
WorkspaceCommandSelection | undefined
> | null = null;
private commandPaletteOpening = false;
buildPaletteCommands(
ctx: CommandBuilderContext
@@ -42,8 +42,36 @@ export class WorkspaceShellCommandPaletteService {
return;
}
if (this.commandPaletteOpening) {
return;
}
const embeddedMpvSupportLoad =
this.playerCommands.ensureEmbeddedMpvSupportLoaded();
if (embeddedMpvSupportLoad) {
this.commandPaletteOpening = true;
void embeddedMpvSupportLoad.finally(() => {
this.commandPaletteOpening = false;
this.openResolvedCommandPalette(ctx, initialQuery);
});
return;
}
this.openResolvedCommandPalette(ctx, initialQuery);
}
private openResolvedCommandPalette(
ctx: CommandBuilderContext,
initialQuery: string
): void {
if (this.commandPaletteRef) {
return;
}
const commands = this.buildPaletteCommands(ctx);
const recentIds = this.recentCommands.entries().map((entry) => entry.id);
const recentIds = this.recentCommands
.entries()
.map((entry) => entry.id);
const dialogRef = this.dialog.open<
WorkspaceCommandPaletteComponent,
{
@@ -111,6 +111,9 @@ describe('WorkspaceShellFacade', () => {
record: jest.Mock;
prune: jest.Mock;
};
let playerCommands: {
ensureEmbeddedMpvSupportLoaded: jest.Mock;
};
let router: {
url: string;
events: ReturnType<typeof of>;
@@ -218,6 +221,9 @@ describe('WorkspaceShellFacade', () => {
record: jest.fn(),
prune: jest.fn(),
};
playerCommands = {
ensureEmbeddedMpvSupportLoaded: jest.fn(),
};
const selectSignal = jest.fn().mockReturnValue(playlistsSignal);
@@ -349,7 +355,7 @@ describe('WorkspaceShellFacade', () => {
},
{
provide: WorkspacePlayerCommandsContributor,
useValue: {},
useValue: playerCommands,
},
],
});
@@ -880,6 +886,27 @@ describe('WorkspaceShellFacade', () => {
expect(recentCommands.record).toHaveBeenCalledWith('open-settings');
});
it('waits for embedded MPV support preload before opening the palette', async () => {
const dialog = TestBed.inject(MatDialog) as unknown as {
open: jest.Mock;
};
let resolveSupport!: () => void;
playerCommands.ensureEmbeddedMpvSupportLoaded.mockReturnValueOnce(
new Promise<void>((resolve) => {
resolveSupport = resolve;
})
);
facade.openCommandPalette();
expect(dialog.open).not.toHaveBeenCalled();
resolveSupport();
await Promise.resolve();
expect(dialog.open).toHaveBeenCalledTimes(1);
});
it('does not record when the palette closes without a selection', () => {
const dialog = TestBed.inject(MatDialog) as unknown as {
open: jest.Mock;