forked from akai/readest
9ad43aa8b2
* Add GHCR and Docker Hub image publishing with fully runtime-configurable pull-first Docker setup (#1) * feat: add container image publishing workflow and pull-based compose setup Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/23c31167-9e15-4d44-ab89-f267b8cd6304 * refine docker publishing workflow and pull-first compose docs Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/23c31167-9e15-4d44-ab89-f267b8cd6304 * chore: temporarily expose docker publish run results for pr branch Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/c946a2f2-2219-4dea-a829-61b287bc4859 * fix: update pinned SHAs for setup-qemu-action and setup-buildx-action to v3 heads Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/0db6957e-476b-48e0-acf3-bee6964c3b32 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * chore: switch all workflow action refs from SHA pins to stable version tags Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/19334b37-9b4c-45df-9c3c-81a497cef8e8 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix: restore missing `with:` blocks lost during SHA-to-tag substitution Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/f204f742-5b7d-4f05-9647-03b9db86ea3d Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix(ci): checkout submodules for docker image workflow Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/72309e8a-6c7c-4004-902a-565f67e5c15f Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * feat(docker): support runtime client env for pulled web image * refactor(web): serve runtime config via script endpoint * fix(web): escape runtime config script payload * fix: align docker runtime config with internal/public backend urls Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/3bbcb608-6202-4f9e-b288-5c95a259da93 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * chore: align published workflow tags with docs * docs: clarify runtime config precedence and linux host mapping Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/3bbcb608-6202-4f9e-b288-5c95a259da93 * refactor: move storage/quota config from build args to runtime env Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/da9b749e-0b1c-47b9-b474-5009765b6ea6 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix: load runtime config in pages router app shell Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/b375132c-8317-4c98-b437-ae48c0153e3d Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix: apply biome formatting for runtime config quota helpers Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/778f5d75-884e-401b-b7cb-4ab40bc64a11 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> Co-authored-by: Amir Pourmand <pourmand1376@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: S3_PUBLIC_ENDPOINT, _document.tsx for beforeInteractive, runtimeConfig fallbacks, README port (#2) Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/4f92f818-c008-4caa-9684-d530500b5fb2 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix: reformat runtimeConfig.ts to satisfy biome formatter (#3) Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/4eea77f4-67ab-4428-b59e-0b21be988037 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
268 lines
12 KiB
TypeScript
268 lines
12 KiB
TypeScript
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
|
|
|
// ── Mocks for constants ──────────────────────────────────────────
|
|
vi.mock('@/services/constants', () => ({
|
|
READEST_WEB_BASE_URL: 'https://web.readest.com',
|
|
READEST_NODE_BASE_URL: 'https://node.readest.com',
|
|
}));
|
|
|
|
// We need to reset modules between tests to pick up env var changes,
|
|
// so we import dynamically in each test or test group.
|
|
|
|
// Cast process.env to a mutable record for test manipulation
|
|
const env = process.env as Record<string, string | undefined>;
|
|
const originalEnv = { ...env };
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
Object.keys(env).forEach((key) => delete env[key]);
|
|
Object.assign(env, originalEnv);
|
|
// Clean up any window globals we set
|
|
delete (window as unknown as Record<string, unknown>)['__READEST_CLI_ACCESS'];
|
|
delete (window as unknown as Record<string, unknown>)['__READEST_RUNTIME_CONFIG'];
|
|
});
|
|
|
|
describe('environment', () => {
|
|
// ── isTauriAppPlatform ─────────────────────────────────────────
|
|
describe('isTauriAppPlatform', () => {
|
|
test('returns true when NEXT_PUBLIC_APP_PLATFORM is tauri', async () => {
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'tauri';
|
|
const { isTauriAppPlatform } = await import('@/services/environment');
|
|
expect(isTauriAppPlatform()).toBe(true);
|
|
});
|
|
|
|
test('returns false when NEXT_PUBLIC_APP_PLATFORM is web', async () => {
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'web';
|
|
const { isTauriAppPlatform } = await import('@/services/environment');
|
|
expect(isTauriAppPlatform()).toBe(false);
|
|
});
|
|
|
|
test('returns false when NEXT_PUBLIC_APP_PLATFORM is not set', async () => {
|
|
delete env['NEXT_PUBLIC_APP_PLATFORM'];
|
|
const { isTauriAppPlatform } = await import('@/services/environment');
|
|
expect(isTauriAppPlatform()).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ── isWebAppPlatform ───────────────────────────────────────────
|
|
describe('isWebAppPlatform', () => {
|
|
test('returns true when NEXT_PUBLIC_APP_PLATFORM is web', async () => {
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'web';
|
|
const { isWebAppPlatform } = await import('@/services/environment');
|
|
expect(isWebAppPlatform()).toBe(true);
|
|
});
|
|
|
|
test('returns false when NEXT_PUBLIC_APP_PLATFORM is tauri', async () => {
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'tauri';
|
|
const { isWebAppPlatform } = await import('@/services/environment');
|
|
expect(isWebAppPlatform()).toBe(false);
|
|
});
|
|
|
|
test('returns false when NEXT_PUBLIC_APP_PLATFORM is not set', async () => {
|
|
delete env['NEXT_PUBLIC_APP_PLATFORM'];
|
|
const { isWebAppPlatform } = await import('@/services/environment');
|
|
expect(isWebAppPlatform()).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ── hasCli ─────────────────────────────────────────────────────
|
|
describe('hasCli', () => {
|
|
test('returns true when __READEST_CLI_ACCESS is true', async () => {
|
|
window.__READEST_CLI_ACCESS = true;
|
|
const { hasCli } = await import('@/services/environment');
|
|
expect(hasCli()).toBe(true);
|
|
});
|
|
|
|
test('returns false when __READEST_CLI_ACCESS is not set', async () => {
|
|
const { hasCli } = await import('@/services/environment');
|
|
expect(hasCli()).toBe(false);
|
|
});
|
|
|
|
test('returns false when __READEST_CLI_ACCESS is explicitly false', async () => {
|
|
window.__READEST_CLI_ACCESS = false;
|
|
const { hasCli } = await import('@/services/environment');
|
|
expect(hasCli()).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ── isPWA ──────────────────────────────────────────────────────
|
|
describe('isPWA', () => {
|
|
test('returns false by default (jsdom matchMedia mock returns false)', async () => {
|
|
const { isPWA } = await import('@/services/environment');
|
|
expect(isPWA()).toBe(false);
|
|
});
|
|
|
|
test('returns true when display-mode is standalone', async () => {
|
|
const originalMatchMedia = window.matchMedia;
|
|
window.matchMedia = vi
|
|
.fn()
|
|
.mockReturnValue({ matches: true }) as unknown as typeof window.matchMedia;
|
|
|
|
const { isPWA } = await import('@/services/environment');
|
|
expect(isPWA()).toBe(true);
|
|
|
|
window.matchMedia = originalMatchMedia;
|
|
});
|
|
});
|
|
|
|
// ── getBaseUrl ─────────────────────────────────────────────────
|
|
describe('getBaseUrl', () => {
|
|
test('returns runtime-configured apiBaseUrl when set', async () => {
|
|
window.__READEST_RUNTIME_CONFIG = {
|
|
apiBaseUrl: 'https://runtime-api.example.com',
|
|
};
|
|
env['NEXT_PUBLIC_API_BASE_URL'] = 'https://custom-api.example.com';
|
|
const { getBaseUrl } = await import('@/services/environment');
|
|
expect(getBaseUrl()).toBe('https://runtime-api.example.com');
|
|
});
|
|
|
|
test('returns API_BASE_URL when set', async () => {
|
|
env['API_BASE_URL'] = 'https://runtime-api.example.com';
|
|
delete env['NEXT_PUBLIC_API_BASE_URL'];
|
|
const { getBaseUrl } = await import('@/services/environment');
|
|
expect(getBaseUrl()).toBe('https://runtime-api.example.com');
|
|
});
|
|
|
|
test('returns NEXT_PUBLIC_API_BASE_URL when set', async () => {
|
|
env['NEXT_PUBLIC_API_BASE_URL'] = 'https://custom-api.example.com';
|
|
const { getBaseUrl } = await import('@/services/environment');
|
|
expect(getBaseUrl()).toBe('https://custom-api.example.com');
|
|
});
|
|
|
|
test('falls back to READEST_WEB_BASE_URL when env var not set', async () => {
|
|
delete env['NEXT_PUBLIC_API_BASE_URL'];
|
|
const { getBaseUrl } = await import('@/services/environment');
|
|
expect(getBaseUrl()).toBe('https://web.readest.com');
|
|
});
|
|
});
|
|
|
|
// ── getNodeBaseUrl ─────────────────────────────────────────────
|
|
describe('getNodeBaseUrl', () => {
|
|
test('returns NEXT_PUBLIC_NODE_BASE_URL when set', async () => {
|
|
env['NEXT_PUBLIC_NODE_BASE_URL'] = 'https://custom-node.example.com';
|
|
const { getNodeBaseUrl } = await import('@/services/environment');
|
|
expect(getNodeBaseUrl()).toBe('https://custom-node.example.com');
|
|
});
|
|
|
|
test('falls back to READEST_NODE_BASE_URL when env var not set', async () => {
|
|
delete env['NEXT_PUBLIC_NODE_BASE_URL'];
|
|
const { getNodeBaseUrl } = await import('@/services/environment');
|
|
expect(getNodeBaseUrl()).toBe('https://node.readest.com');
|
|
});
|
|
});
|
|
|
|
// ── isMacPlatform ──────────────────────────────────────────────
|
|
describe('isMacPlatform', () => {
|
|
test('returns true when navigator.platform contains Mac', async () => {
|
|
Object.defineProperty(navigator, 'platform', { value: 'MacIntel', configurable: true });
|
|
const { isMacPlatform } = await import('@/services/environment');
|
|
expect(isMacPlatform()).toBe(true);
|
|
});
|
|
|
|
test('returns true when navigator.platform is iPhone', async () => {
|
|
Object.defineProperty(navigator, 'platform', { value: 'iPhone', configurable: true });
|
|
const { isMacPlatform } = await import('@/services/environment');
|
|
expect(isMacPlatform()).toBe(true);
|
|
});
|
|
|
|
test('returns true when navigator.platform is iPad', async () => {
|
|
Object.defineProperty(navigator, 'platform', { value: 'iPad', configurable: true });
|
|
const { isMacPlatform } = await import('@/services/environment');
|
|
expect(isMacPlatform()).toBe(true);
|
|
});
|
|
|
|
test('returns true when navigator.platform is iPod', async () => {
|
|
Object.defineProperty(navigator, 'platform', { value: 'iPod', configurable: true });
|
|
const { isMacPlatform } = await import('@/services/environment');
|
|
expect(isMacPlatform()).toBe(true);
|
|
});
|
|
|
|
test('returns false when navigator.platform is Win32', async () => {
|
|
Object.defineProperty(navigator, 'platform', { value: 'Win32', configurable: true });
|
|
const { isMacPlatform } = await import('@/services/environment');
|
|
expect(isMacPlatform()).toBe(false);
|
|
});
|
|
|
|
test('returns false when navigator.platform is Linux', async () => {
|
|
Object.defineProperty(navigator, 'platform', { value: 'Linux x86_64', configurable: true });
|
|
const { isMacPlatform } = await import('@/services/environment');
|
|
expect(isMacPlatform()).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ── getCommandPaletteShortcut ──────────────────────────────────
|
|
describe('getCommandPaletteShortcut', () => {
|
|
test('returns Mac shortcut on Mac platforms', async () => {
|
|
Object.defineProperty(navigator, 'platform', { value: 'MacIntel', configurable: true });
|
|
const { getCommandPaletteShortcut } = await import('@/services/environment');
|
|
expect(getCommandPaletteShortcut()).toContain('P');
|
|
});
|
|
|
|
test('returns Ctrl shortcut on non-Mac platforms', async () => {
|
|
Object.defineProperty(navigator, 'platform', { value: 'Win32', configurable: true });
|
|
const { getCommandPaletteShortcut } = await import('@/services/environment');
|
|
expect(getCommandPaletteShortcut()).toBe('Ctrl+Shift+P');
|
|
});
|
|
});
|
|
|
|
// ── getAPIBaseUrl ──────────────────────────────────────────────
|
|
describe('getAPIBaseUrl', () => {
|
|
test('returns /api in web development mode', async () => {
|
|
env['NODE_ENV'] = 'development';
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'web';
|
|
const { getAPIBaseUrl } = await import('@/services/environment');
|
|
expect(getAPIBaseUrl()).toBe('/api');
|
|
});
|
|
|
|
test('returns full URL in production', async () => {
|
|
env['NODE_ENV'] = 'production';
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'web';
|
|
delete env['NEXT_PUBLIC_API_BASE_URL'];
|
|
const { getAPIBaseUrl } = await import('@/services/environment');
|
|
expect(getAPIBaseUrl()).toBe('https://web.readest.com/api');
|
|
});
|
|
|
|
test('returns full URL for tauri platform even in development', async () => {
|
|
env['NODE_ENV'] = 'development';
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'tauri';
|
|
delete env['NEXT_PUBLIC_API_BASE_URL'];
|
|
const { getAPIBaseUrl } = await import('@/services/environment');
|
|
expect(getAPIBaseUrl()).toBe('https://web.readest.com/api');
|
|
});
|
|
});
|
|
|
|
// ── getNodeAPIBaseUrl ──────────────────────────────────────────
|
|
describe('getNodeAPIBaseUrl', () => {
|
|
test('returns /api in web development mode', async () => {
|
|
env['NODE_ENV'] = 'development';
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'web';
|
|
const { getNodeAPIBaseUrl } = await import('@/services/environment');
|
|
expect(getNodeAPIBaseUrl()).toBe('/api');
|
|
});
|
|
|
|
test('returns full node URL in production', async () => {
|
|
env['NODE_ENV'] = 'production';
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'web';
|
|
delete env['NEXT_PUBLIC_NODE_BASE_URL'];
|
|
const { getNodeAPIBaseUrl } = await import('@/services/environment');
|
|
expect(getNodeAPIBaseUrl()).toBe('https://node.readest.com/api');
|
|
});
|
|
|
|
test('returns full node URL for tauri platform even in development', async () => {
|
|
env['NODE_ENV'] = 'development';
|
|
env['NEXT_PUBLIC_APP_PLATFORM'] = 'tauri';
|
|
delete env['NEXT_PUBLIC_NODE_BASE_URL'];
|
|
const { getNodeAPIBaseUrl } = await import('@/services/environment');
|
|
expect(getNodeAPIBaseUrl()).toBe('https://node.readest.com/api');
|
|
});
|
|
});
|
|
|
|
// ── environmentConfig default export ───────────────────────────
|
|
describe('environmentConfig', () => {
|
|
test('exports an object with getAppService function', async () => {
|
|
const envConfig = await import('@/services/environment');
|
|
expect(typeof envConfig.default.getAppService).toBe('function');
|
|
});
|
|
});
|
|
});
|