Skip to content
Draft
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
59 changes: 47 additions & 12 deletions core/src/utils/native/status-bar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { SystemBars } from '@capacitor/core';
import type { StatusBarPlugin, Style as StatusBarStyle } from '@capacitor/status-bar';

import { getCapacitor } from './capacitor';

type SystemBarsPlugin = typeof SystemBars;
type SystemBarsStyleOptions = Parameters<SystemBarsPlugin['setStyle']>[0];

interface StyleOptions {
style: StatusBarStyle;
}
Expand All @@ -12,29 +16,60 @@ export enum Style {
Default = 'DEFAULT',
}

export const StatusBar = {
getEngine(): StatusBarPlugin | undefined {
const capacitor = getCapacitor();
/**
* The external @capacitor/status-bar plugin is preferred because it can
* report the current style. Capacitor 8+ ships the built-in SystemBars
* plugin, which is used when the external plugin is not installed.
*/
type ResolvedEngine =
| { kind: 'StatusBar'; plugin: StatusBarPlugin }
| { kind: 'SystemBars'; plugin: SystemBarsPlugin }
| undefined;

if (capacitor?.isPluginAvailable('StatusBar')) {
return capacitor.Plugins.StatusBar as StatusBarPlugin;
}
return undefined;
const resolveEngine = (): ResolvedEngine => {
const capacitor = getCapacitor();

if (capacitor?.isPluginAvailable('StatusBar')) {
return { kind: 'StatusBar', plugin: capacitor.Plugins.StatusBar as StatusBarPlugin };
}
if (capacitor?.isPluginAvailable('SystemBars')) {
return { kind: 'SystemBars', plugin: capacitor.Plugins.SystemBars as SystemBarsPlugin };
}
return undefined;
};

export const StatusBar = {
getEngine(): StatusBarPlugin | SystemBarsPlugin | undefined {
return resolveEngine()?.plugin;
},
setStyle(options: StyleOptions) {
const engine = this.getEngine();
const engine = resolveEngine();
if (!engine) {
return;
}

engine.setStyle(options);
if (engine.kind === 'SystemBars') {
/**
* Only restyle the status bar so the navigation/gesture bar is
* left untouched. String literals are used instead of Capacitor's
* runtime enums to avoid importing @capacitor/core at runtime.
*/
engine.plugin.setStyle({ style: options.style, bar: 'StatusBar' } as unknown as SystemBarsStyleOptions);
return;
}

engine.plugin.setStyle(options);
},
getStyle: async function (): Promise<StatusBarStyle> {
const engine = this.getEngine();
if (!engine) {
const engine = resolveEngine();
/**
* SystemBars has no getter, so DEFAULT (device appearance) is the
* style restored on dismiss.
*/
if (engine?.kind !== 'StatusBar') {
return Style.Default;
}
const { style } = await engine.getInfo();
const { style } = await engine.plugin.getInfo();
return style;
},
};
138 changes: 138 additions & 0 deletions core/src/utils/native/test/status-bar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { StatusBar, Style } from '../status-bar';

const createStatusBarPlugin = () => ({
setStyle: jest.fn(),
getInfo: jest.fn().mockResolvedValue({ style: Style.Light, visible: true, overlays: false }),
});

/**
* Installs a fake window.Capacitor. `plugins` are registered under
* `Capacitor.Plugins`, and `isPluginAvailable` reports only the
* names listed in `available`.
*/
const mockCapacitor = ({ plugins, available }: { plugins: Record<string, unknown>; available: string[] }) => {
(window as any).Capacitor = {
isPluginAvailable: (name: string) => available.includes(name),
Plugins: plugins,
};
};

/**
* Assertions use raw style strings rather than the Style enum so they
* pin the values the native plugins actually receive.
*/
describe('StatusBar', () => {
afterEach(() => {
delete (window as any).Capacitor;
});

describe('when the StatusBar plugin is available', () => {
let statusBarPlugin: ReturnType<typeof createStatusBarPlugin>;

beforeEach(() => {
statusBarPlugin = createStatusBarPlugin();
mockCapacitor({ plugins: { StatusBar: statusBarPlugin }, available: ['StatusBar'] });
});

it('should forward the style to the StatusBar plugin', () => {
StatusBar.setStyle({ style: Style.Dark });

expect(statusBarPlugin.setStyle.mock.calls).toStrictEqual([[{ style: 'DARK' }]]);
});

it('should resolve getStyle to the style reported by the StatusBar plugin', async () => {
await expect(StatusBar.getStyle()).resolves.toBe('LIGHT');
});
});

describe('when Capacitor is present but the StatusBar plugin is not registered', () => {
beforeEach(() => {
mockCapacitor({ plugins: {}, available: [] });
});

it('should not throw when setting the style', () => {
expect(() => StatusBar.setStyle({ style: Style.Dark })).not.toThrow();
});

it('should resolve getStyle to DEFAULT', async () => {
await expect(StatusBar.getStyle()).resolves.toBe('DEFAULT');
});
});

describe('when the StatusBar plugin is registered but not available', () => {
let statusBarPlugin: ReturnType<typeof createStatusBarPlugin>;

beforeEach(() => {
/**
* The plugin is registered under Plugins but not available natively,
* so the wrapper must rely on isPluginAvailable rather than on the
* presence of Plugins.StatusBar.
*/
statusBarPlugin = createStatusBarPlugin();
mockCapacitor({ plugins: { StatusBar: statusBarPlugin }, available: [] });
});

it('should not call the StatusBar plugin when setting the style', () => {
expect(() => StatusBar.setStyle({ style: Style.Dark })).not.toThrow();

expect(statusBarPlugin.setStyle).not.toHaveBeenCalled();
});

it('should resolve getStyle to DEFAULT without querying the StatusBar plugin', async () => {
await expect(StatusBar.getStyle()).resolves.toBe('DEFAULT');

expect(statusBarPlugin.getInfo).not.toHaveBeenCalled();
});
});

describe('when only the SystemBars plugin is available', () => {
let systemBarsPlugin: { setStyle: jest.Mock };

beforeEach(() => {
systemBarsPlugin = { setStyle: jest.fn() };
mockCapacitor({ plugins: { SystemBars: systemBarsPlugin }, available: ['SystemBars'] });
});

it('should style only the status bar through the SystemBars plugin', () => {
StatusBar.setStyle({ style: Style.Dark });

expect(systemBarsPlugin.setStyle.mock.calls).toStrictEqual([[{ style: 'DARK', bar: 'StatusBar' }]]);
});

it('should resolve getStyle to DEFAULT', async () => {
await expect(StatusBar.getStyle()).resolves.toBe('DEFAULT');
});
});

describe('when both the StatusBar and SystemBars plugins are available', () => {
let statusBarPlugin: ReturnType<typeof createStatusBarPlugin>;
let systemBarsPlugin: { setStyle: jest.Mock };

beforeEach(() => {
statusBarPlugin = createStatusBarPlugin();
systemBarsPlugin = { setStyle: jest.fn() };
mockCapacitor({
plugins: { StatusBar: statusBarPlugin, SystemBars: systemBarsPlugin },
available: ['StatusBar', 'SystemBars'],
});
});

it('should only call the StatusBar plugin', async () => {
StatusBar.setStyle({ style: Style.Dark });

await expect(StatusBar.getStyle()).resolves.toBe('LIGHT');
expect(statusBarPlugin.setStyle.mock.calls).toStrictEqual([[{ style: 'DARK' }]]);
expect(systemBarsPlugin.setStyle).not.toHaveBeenCalled();
});
});

describe('when Capacitor is not present', () => {
it('should not throw when setting the style', () => {
expect(() => StatusBar.setStyle({ style: Style.Dark })).not.toThrow();
});

it('should resolve getStyle to DEFAULT', async () => {
await expect(StatusBar.getStyle()).resolves.toBe('DEFAULT');
});
});
});
Loading