feat(ext/setup): hand off completion to fullscreen vault tab (P2)

After successful device registration (state.configPushed = true), the
wizard now opens vault.html in a new tab and closes the setup tab.
Both create-new and attach-existing flows funnel through the same
finishSetup() handler. Closing the setup tab is best-effort --
chrome.tabs.remove failures don't block the vault open.

Add src/__stubs__/relicario_wasm.stub.ts + vitest.config alias so
setup.ts can be imported in unit tests without the runtime WASM file.
Exclude the stubs dir from the webpack/tsc build in tsconfig.json.
This commit is contained in:
adlee-was-taken
2026-05-02 19:15:35 -04:00
parent 631e9af470
commit 4e9d834920
5 changed files with 77 additions and 1 deletions

View File

@@ -0,0 +1,13 @@
// Stub for the runtime-only WASM module. Used by vitest so that modules
// importing relicario_wasm.js can be loaded in a Node/happy-dom environment.
// Individual tests that exercise WASM calls should mock the relevant exports.
export default async function init(): Promise<void> {}
export const unlock = (): never => { throw new Error('wasm stub: unlock not mocked'); };
export const lock = (): void => {};
export const manifest_encrypt = (): never => { throw new Error('wasm stub: manifest_encrypt not mocked'); };
export const manifest_decrypt = (): never => { throw new Error('wasm stub: manifest_decrypt not mocked'); };
export const settings_encrypt = (): never => { throw new Error('wasm stub: settings_encrypt not mocked'); };
export const default_vault_settings_json = (): string => '{}';
export const embed_image_secret = (): never => { throw new Error('wasm stub: embed_image_secret not mocked'); };
export const register_device = (): never => { throw new Error('wasm stub: register_device not mocked'); };

View File

@@ -0,0 +1,37 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { finishSetup } from '../setup';
describe('finishSetup', () => {
beforeEach(() => {
(global as any).chrome = {
tabs: {
create: vi.fn(() => Promise.resolve({ id: 999 })),
getCurrent: vi.fn(() => Promise.resolve({ id: 42 })),
remove: vi.fn(() => Promise.resolve()),
},
runtime: {
getURL: vi.fn((p: string) => `chrome-extension://abc/${p}`),
},
};
});
it('opens vault.html in a new tab', async () => {
await finishSetup();
expect(chrome.runtime.getURL).toHaveBeenCalledWith('vault.html');
expect(chrome.tabs.create).toHaveBeenCalledWith({
url: 'chrome-extension://abc/vault.html',
});
});
it('closes the current setup tab after opening the vault tab', async () => {
await finishSetup();
expect(chrome.tabs.getCurrent).toHaveBeenCalled();
expect(chrome.tabs.remove).toHaveBeenCalledWith(42);
});
it('still opens the vault tab even if closing the setup tab fails', async () => {
(chrome.tabs.remove as any).mockRejectedValueOnce(new Error('no permission'));
await expect(finishSetup()).resolves.not.toThrow();
expect(chrome.tabs.create).toHaveBeenCalled();
});
});

View File

@@ -1101,6 +1101,7 @@ function attachStep5(): void {
state.configPushed = true;
render();
void finishSetup();
} catch (err: unknown) {
console.error('[relicario setup] register device failed:', err);
state.error = `Failed to register device: ${err instanceof Error ? err.message : String(err)}`;
@@ -1131,6 +1132,23 @@ function attachStep5(): void {
});
}
// --- Completion handoff ---
/// Open the fullscreen vault tab and best-effort close the setup tab.
export async function finishSetup(): Promise<void> {
const vaultUrl = chrome.runtime.getURL('vault.html');
await chrome.tabs.create({ url: vaultUrl });
try {
const current = await chrome.tabs.getCurrent();
if (current?.id !== undefined) {
await chrome.tabs.remove(current.id);
}
} catch {
// Setup tab may not be closeable (e.g., opened as popup rather than a tab).
// The vault tab is open — that's the user-visible success.
}
}
// --- Boot ---
document.addEventListener('DOMContentLoaded', () => {