diff --git a/CHANGELOG.md b/CHANGELOG.md index 839eaa92f..60ee76afa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## v1.12.9 + +* fix(admob): remove the policy-prohibited interstitial shown while exiting the app + ## v1.12.8 * fix(terminal): close tab on process end and speed up long-press select by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/2587 diff --git a/config.xml b/config.xml index 51ae51314..1667557e8 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - diff --git a/package-lock.json b/package-lock.json index 795065ad4..2facaba1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "com.foxdebug.acode", - "version": "1.12.8", + "version": "1.12.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "com.foxdebug.acode", - "version": "1.12.8", + "version": "1.12.9", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.20.3", diff --git a/package.json b/package.json index 4932a15f2..30297fa42 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.foxdebug.acode", "displayName": "Acode", - "version": "1.12.8", + "version": "1.12.9", "description": "Acode is a code editor for android", "scripts": { "lang": "node ./utils/lang.js", diff --git a/src/lib/actionStack.js b/src/lib/actionStack.js index 915898056..2f2215ba6 100644 --- a/src/lib/actionStack.js +++ b/src/lib/actionStack.js @@ -1,6 +1,5 @@ import confirm from "dialogs/confirm"; import appSettings from "lib/settings"; -import helpers from "utils/helpers"; const stack = []; let mark = null; @@ -94,8 +93,6 @@ export default { } } - helpers.showInterstitialIfReady(); - exitApp(); } }, diff --git a/tests/unit/actionStack.test.js b/tests/unit/actionStack.test.js new file mode 100644 index 000000000..7bfe827c0 --- /dev/null +++ b/tests/unit/actionStack.test.js @@ -0,0 +1,126 @@ +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + confirm: vi.fn(), + exitApp: vi.fn(), + settings: { + value: { + confirmOnExit: false, + }, + }, + showInterstitialIfReady: vi.fn(), +})); + +vi.mock("dialogs/confirm", () => ({ + default: mocks.confirm, +})); + +vi.mock("lib/settings", () => ({ + default: mocks.settings, +})); + +// Keep this mock as a policy regression guard. If the exit flow starts importing +// the ad helper again, the assertions below will catch the prohibited placement. +vi.mock("utils/helpers", () => ({ + default: { + showInterstitialIfReady: mocks.showInterstitialIfReady, + }, +})); + +import actionStack from "lib/actionStack"; + +const originalNavigatorDescriptor = Object.getOwnPropertyDescriptor( + globalThis, + "navigator", +); +const originalAcode = globalThis.acode; +const originalStrings = globalThis.strings; + +function setNavigator() { + Object.defineProperty(globalThis, "navigator", { + configurable: true, + value: { + app: { + exitApp: mocks.exitApp, + }, + }, + }); +} + +describe("actionStack app exit", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.settings.value.confirmOnExit = false; + actionStack.onCloseApp = undefined; + actionStack.unfreeze(); + globalThis.acode = { exitAppMessage: "Close Acode?" }; + globalThis.strings = { warning: "Warning" }; + setNavigator(); + }); + + afterAll(() => { + actionStack.onCloseApp = undefined; + globalThis.acode = originalAcode; + globalThis.strings = originalStrings; + + if (originalNavigatorDescriptor) { + Object.defineProperty( + globalThis, + "navigator", + originalNavigatorDescriptor, + ); + } else { + Reflect.deleteProperty(globalThis, "navigator"); + } + }); + + it("exits without showing an interstitial after confirmation", async () => { + mocks.settings.value.confirmOnExit = true; + mocks.confirm.mockResolvedValue(true); + + await actionStack.pop(); + + expect(mocks.confirm).toHaveBeenCalledWith("WARNING", "Close Acode?"); + expect(mocks.showInterstitialIfReady).not.toHaveBeenCalled(); + expect(mocks.exitApp).toHaveBeenCalledOnce(); + }); + + it("does not show an interstitial or exit when confirmation is cancelled", async () => { + mocks.settings.value.confirmOnExit = true; + mocks.confirm.mockResolvedValue(false); + + await actionStack.pop(); + + expect(mocks.showInterstitialIfReady).not.toHaveBeenCalled(); + expect(mocks.exitApp).not.toHaveBeenCalled(); + }); + + it("runs a synchronous close callback before exiting", async () => { + const onClose = vi.fn(); + actionStack.onCloseApp = onClose; + + await actionStack.pop(); + + expect(onClose).toHaveBeenCalledOnce(); + expect(mocks.showInterstitialIfReady).not.toHaveBeenCalled(); + expect(mocks.exitApp).toHaveBeenCalledOnce(); + }); + + it("waits for an asynchronous close callback before exiting", async () => { + let finishClose; + const closeComplete = new Promise((resolve) => { + finishClose = resolve; + }); + actionStack.onCloseApp = vi.fn(() => closeComplete); + + await actionStack.pop(); + + expect(mocks.exitApp).not.toHaveBeenCalled(); + finishClose(); + await closeComplete; + await Promise.resolve(); + + expect(mocks.showInterstitialIfReady).not.toHaveBeenCalled(); + expect(mocks.exitApp).toHaveBeenCalledOnce(); + }); +});