fix(gui): turn on can't run twice at once

The browser card's button stays out while the client is enabling or
disabling, through any redraw in between, and a status read begun before
the press no longer draws the card.
This commit is contained in:
Juan Castro committed 2026-10-27 14:20:17 -04:00
1 parent d77ebadfe6
commit 389fd26bee
2 files changed
+111 -11

No files matched your search

+25 -11
View File
@@ -85,11 +85,12 @@ const BROWSER_DETAIL_KEYS = {
off: 'push_browser_off',
};
/** @param {BrowserPushState} state */
export const browserCardHtml = (state) => {
/** @param {BrowserPushState} state @param {{ busy?: boolean }} [opts] - `busy` while the client is enabling or disabling */
export const browserCardHtml = (state, { busy = false } = {}) => {
const disabled = busy ? ' disabled' : '';
let action = '';
if ( state === 'on' ) action = `<button type="button" class="button push-turn-off">${i18n('push_turn_off')}</button>`;
if ( state === 'off' ) action = `<button type="button" class="button push-turn-on">${i18n('push_turn_on')}</button>`;
if ( state === 'on' ) action = `<button type="button" class="button push-turn-off"${disabled}>${i18n('push_turn_off')}</button>`;
if ( state === 'off' ) action = `<button type="button" class="button push-turn-on"${disabled}>${i18n('push_turn_on')}</button>`;
return card({
icon: ICONS.bell,
title: i18n('push_this_browser'),
@@ -200,12 +201,27 @@ const TabNotifications = {
// Only the latest status read is drawn, so a slow one can't undo a newer state.
let browserSeq = 0;
let browserBusy = false;
const refreshBrowser = async () => {
const seq = ++browserSeq;
const state = await browserPushState(getPushClient());
if ( seq === browserSeq ) $section.find('.push-browser-card').replaceWith(browserCardHtml(state));
if ( seq === browserSeq ) {
$section.find('.push-browser-card').replaceWith(browserCardHtml(state, { busy: browserBusy }));
}
return state;
};
// One client call at a time: the card's button is out until it settles,
// and a status read begun before it no longer draws the card.
const whileBrowserBusy = async (call) => {
browserBusy = true;
browserSeq++;
$section.find('.push-browser-card button').prop('disabled', true);
try {
return await call();
} finally {
browserBusy = false;
}
};
const renderDevices = () => {
const now = Date.now();
@@ -275,11 +291,10 @@ const TabNotifications = {
$section.on('click', '.push-turn-on', async function () {
const client = getPushClient();
if ( ! client ) return;
$(this).prop('disabled', true);
if ( ! client || browserBusy ) return;
let deviceUid;
try {
({ deviceUid } = await client.enable());
({ deviceUid } = await whileBrowserBusy(() => client.enable()));
} catch ( e ) {
console.warn('[push] enable failed', e);
if ( await refreshBrowser() !== 'blocked' ) showError('push_enable_failed', $el_window);
@@ -301,10 +316,9 @@ const TabNotifications = {
$section.on('click', '.push-turn-off', async function () {
const client = getPushClient();
if ( ! client ) return;
$(this).prop('disabled', true);
if ( ! client || browserBusy ) return;
try {
await client.disable();
await whileBrowserBusy(() => client.disable());
} catch ( e ) {
console.warn('[push] disable failed', e);
showError('push_disable_failed', $el_window);
@@ -241,6 +241,92 @@ describe('this browser', () => {
expect(browserCard().hasClass('push-browser-on')).toBe(true);
});
it('turns on once however often the button is pressed while enabling', async () => {
const client = fakeClient();
pushClient.registerPushClient(client);
let finishEnable;
client.enable.mockImplementation(() => new Promise(resolve => {
finishEnable = resolve;
}));
serve({
'GET /push/devices': { devices: [] },
'GET /push/apps': { apps: [] },
'GET /push/preferences': { puter: true },
'POST /push/devices/dev-new/test': () => reply(200, { sent: true }),
});
await open();
$('.push-turn-on').trigger('click');
await settle();
expect($('.push-turn-on').prop('disabled')).toBe(true);
// A redraw while enabling keeps the button out.
await tab.onActivate($('.win'));
await settle();
expect($('.push-turn-on').prop('disabled')).toBe(true);
$('.push-turn-on').trigger('click');
$('.push-turn-on').prop('disabled', false).trigger('click');
await settle();
expect(client.enable).toHaveBeenCalledTimes(1);
client.status.mockResolvedValue({ permission: 'granted', registered: true, deviceUid: 'dev-new' });
finishEnable({ deviceUid: 'dev-new' });
await settle();
expect(browserCard().hasClass('push-browser-on')).toBe(true);
expect($('.push-turn-off').prop('disabled')).toBe(false);
});
it('lets no status read begun before Turn on draw the card while enabling', async () => {
const client = fakeClient();
pushClient.registerPushClient(client);
await open();
let finishSlow;
client.status.mockImplementationOnce(() => new Promise(resolve => {
finishSlow = resolve;
}));
let finishEnable;
client.enable.mockImplementation(() => new Promise(resolve => {
finishEnable = resolve;
}));
tab.onActivate($('.win'));
await settle();
$('.push-turn-on').trigger('click');
await settle();
finishSlow({ permission: 'default', registered: false });
await settle();
expect($('.push-turn-on').prop('disabled')).toBe(true);
client.status.mockResolvedValue({ permission: 'granted', registered: true, deviceUid: 'dev-new' });
finishEnable({ deviceUid: 'dev-new' });
await settle();
expect(browserCard().hasClass('push-browser-on')).toBe(true);
});
it('turns off once however often the button is pressed while disabling', async () => {
const client = fakeClient({ permission: 'granted', registered: true, deviceUid: 'dev-1' });
pushClient.registerPushClient(client);
let finishDisable;
client.disable.mockImplementation(() => new Promise(resolve => {
finishDisable = resolve;
}));
await open();
$('.push-turn-off').trigger('click');
await settle();
await tab.onActivate($('.win'));
await settle();
expect($('.push-turn-off').prop('disabled')).toBe(true);
$('.push-turn-off').prop('disabled', false).trigger('click');
await settle();
expect(client.disable).toHaveBeenCalledTimes(1);
client.status.mockResolvedValue({ permission: 'granted', registered: false });
finishDisable();
await settle();
expect(browserCard().hasClass('push-browser-off')).toBe(true);
expect($('.push-turn-on').prop('disabled')).toBe(false);
});
it('turns off through the client', async () => {
const client = fakeClient({ permission: 'granted', registered: true, deviceUid: 'dev-1' });
pushClient.registerPushClient(client);