Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/lib/core/LocationFull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BeforeNavigateEvent, Events, NavigationCancelledEvent, NavigationE
import { isConformantState } from "./isConformantState.js";
import { LocationLite } from "./LocationLite.svelte.js";
import { LocationState } from "./LocationState.svelte.js";
import { logger } from "./Logger.js";

/**
* Location implementation of the library's full mode feature.
Expand Down Expand Up @@ -59,7 +60,7 @@ export class LocationFull extends LocationLite {
}
} else {
if (!isConformantState(event.state)) {
console.warn("Warning: Non-conformant state object passed to history." + method + "State. Previous state will prevail.");
logger.warn(`Warning: Non-conformant state object passed to history.${method}State. Previous state will prevail.`);
event.state = this.#innerState.state;
}
const navFn = method === 'push' ? this.#originalPushState : this.#originalReplaceState;
Expand Down
23 changes: 12 additions & 11 deletions src/lib/core/LocationState.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { describe, test, expect, beforeEach, vi } from 'vitest';
import { LocationState } from './LocationState.svelte.js';
import type { State } from '$lib/types.js';
import { logger } from './Logger.js';

// Mock console.warn to capture warnings
const mockConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
// Mock logger.warn to capture warnings
const mockLoggerWarn = vi.spyOn(logger, 'warn').mockImplementation(() => {});

// Mock globalThis.window for testing
const mockWindow = {
Expand All @@ -17,7 +18,7 @@ const mockWindow = {

describe('LocationState', () => {
beforeEach(() => {
mockConsoleWarn.mockClear();
mockLoggerWarn.mockClear();
// Reset to default mock state
mockWindow.history.state = null;
vi.stubGlobal('window', mockWindow);
Expand All @@ -37,7 +38,7 @@ describe('LocationState', () => {

// Assert
expect(locationState.state).toStrictEqual(validState); // Deep equality since $state() creates new objects
expect(mockConsoleWarn).not.toHaveBeenCalled();
expect(mockLoggerWarn).not.toHaveBeenCalled();
});

test('Should handle conformant state with undefined path.', () => {
Expand All @@ -53,7 +54,7 @@ describe('LocationState', () => {

// Assert
expect(locationState.state).toStrictEqual(validState);
expect(mockConsoleWarn).not.toHaveBeenCalled();
expect(mockLoggerWarn).not.toHaveBeenCalled();
});

test('Should handle conformant state with empty hash object.', () => {
Expand All @@ -69,7 +70,7 @@ describe('LocationState', () => {

// Assert
expect(locationState.state).toStrictEqual(validState);
expect(mockConsoleWarn).not.toHaveBeenCalled();
expect(mockLoggerWarn).not.toHaveBeenCalled();
});

test('Should handle conformant state with numeric path.', () => {
Expand All @@ -85,7 +86,7 @@ describe('LocationState', () => {

// Assert
expect(locationState.state).toStrictEqual(validState);
expect(mockConsoleWarn).not.toHaveBeenCalled();
expect(mockLoggerWarn).not.toHaveBeenCalled();
});
});

Expand All @@ -102,7 +103,7 @@ describe('LocationState', () => {
path: undefined,
hash: {}
});
expect(mockConsoleWarn).not.toHaveBeenCalled();
expect(mockLoggerWarn).not.toHaveBeenCalled();
});

test('Should create clean state when history.state is undefined without warning.', () => {
Expand All @@ -117,7 +118,7 @@ describe('LocationState', () => {
path: undefined,
hash: {}
});
expect(mockConsoleWarn).not.toHaveBeenCalled();
expect(mockLoggerWarn).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -155,8 +156,8 @@ describe('LocationState', () => {
path: undefined,
hash: {}
});
expect(mockConsoleWarn).toHaveBeenCalledOnce();
expect(mockConsoleWarn).toHaveBeenCalledWith(
expect(mockLoggerWarn).toHaveBeenCalledOnce();
expect(mockLoggerWarn).toHaveBeenCalledWith(
'Non-conformant state data detected in History API. Resetting to clean state.'
);
});
Expand Down
3 changes: 2 additions & 1 deletion src/lib/core/LocationState.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SvelteURL } from "svelte/reactivity";
import { isConformantState } from "./isConformantState.js";
import { logger } from "./Logger.js";

/**
* Helper class used to manage the reactive data of Location implementations.
Expand All @@ -14,7 +15,7 @@ export class LocationState {
let validState = false;
this.state = $state((validState = isConformantState(historyState)) ? historyState : { path: undefined, hash: {} });
if (!validState && historyState != null) {
console.warn('Non-conformant state data detected in History API. Resetting to clean state.');
logger.warn('Non-conformant state data detected in History API. Resetting to clean state.');
}
}
}
161 changes: 161 additions & 0 deletions src/lib/core/Logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
import { logger, setLogger } from "./Logger.js";
import type { ILogger } from "$lib/types.js";

describe("logger", () => {
test("Should default to globalThis.console", () => {
expect(logger).toBe(globalThis.console);
});
});

describe("setLogger", () => {
let originalLogger: ILogger;
let mockLogger: ILogger;
let consoleSpy: {
debug: any;
log: any;
warn: any;
error: any;
};

beforeEach(() => {
// Store original logger state
originalLogger = logger;

// Create mock logger
mockLogger = {
debug: vi.fn(),
log: vi.fn(),
warn: vi.fn(),
error: vi.fn()
};

// Create console spies
consoleSpy = {
debug: vi.spyOn(globalThis.console, 'debug').mockImplementation(() => {}),
log: vi.spyOn(globalThis.console, 'log').mockImplementation(() => {}),
warn: vi.spyOn(globalThis.console, 'warn').mockImplementation(() => {}),
error: vi.spyOn(globalThis.console, 'error').mockImplementation(() => {})
};
});

afterEach(() => {
// Restore original state
setLogger(originalLogger);
vi.restoreAllMocks();
});

describe("Boolean arguments", () => {
test("Should set logger to globalThis.console when true", () => {
setLogger(true);

expect(logger).toBe(globalThis.console);

logger.debug("test");
expect(consoleSpy.debug).toHaveBeenCalledWith("test");
});

test("Should set logger to noop functions when false", () => {
setLogger(false);

expect(logger).not.toBe(globalThis.console);

// Should not throw and should not call console
logger.debug("debug message");
logger.log("log message");
logger.warn("warn message");
logger.error("error message");

expect(consoleSpy.debug).not.toHaveBeenCalled();
expect(consoleSpy.log).not.toHaveBeenCalled();
expect(consoleSpy.warn).not.toHaveBeenCalled();
expect(consoleSpy.error).not.toHaveBeenCalled();
});

test("Should allow switching between true and false", () => {
setLogger(true);
logger.log("enabled message");
expect(consoleSpy.log).toHaveBeenCalledWith("enabled message");

setLogger(false);
logger.log("disabled message");
expect(consoleSpy.log).toHaveBeenCalledTimes(1); // Should not be called again

setLogger(true);
logger.log("re-enabled message");
expect(consoleSpy.log).toHaveBeenCalledWith("re-enabled message");
expect(consoleSpy.log).toHaveBeenCalledTimes(2);
});
});

describe("ILogger implementations", () => {
test("Should set custom logger as the global logger", () => {
setLogger(mockLogger);

expect(logger).toBe(mockLogger);

logger.debug("custom debug");
logger.log("custom log");
logger.warn("custom warn");
logger.error("custom error");

expect(mockLogger.debug).toHaveBeenCalledWith("custom debug");
expect(mockLogger.log).toHaveBeenCalledWith("custom log");
expect(mockLogger.warn).toHaveBeenCalledWith("custom warn");
expect(mockLogger.error).toHaveBeenCalledWith("custom error");

// Console should not be called
expect(consoleSpy.debug).not.toHaveBeenCalled();
expect(consoleSpy.log).not.toHaveBeenCalled();
expect(consoleSpy.warn).not.toHaveBeenCalled();
expect(consoleSpy.error).not.toHaveBeenCalled();
});

test("Should work with extended logger implementations", () => {
const extendedLogger = {
...mockLogger,
trace: vi.fn(),
info: vi.fn()
};

setLogger(extendedLogger);

expect(logger).toBe(extendedLogger);

logger.debug("debug");
logger.log("log");
logger.warn("warn");
logger.error("error");

expect(extendedLogger.debug).toHaveBeenCalledWith("debug");
expect(extendedLogger.log).toHaveBeenCalledWith("log");
expect(extendedLogger.warn).toHaveBeenCalledWith("warn");
expect(extendedLogger.error).toHaveBeenCalledWith("error");
});

test("Should allow switching from custom logger back to stock logger", () => {
setLogger(mockLogger);
logger.log("custom message");
expect(mockLogger.log).toHaveBeenCalledWith("custom message");

setLogger(true);
logger.log("stock message");
expect(consoleSpy.log).toHaveBeenCalledWith("stock message");
expect(mockLogger.log).toHaveBeenCalledTimes(1); // Should not be called again
});

test("Should handle multiple parameters correctly", () => {
setLogger(mockLogger);

logger.debug("debug", 123, { key: "value" });
logger.log("log", true, null);
logger.warn("warn", "multiple", "parameters");
logger.error("error", { error: "object" });

expect(mockLogger.debug).toHaveBeenCalledWith("debug", 123, { key: "value" });
expect(mockLogger.log).toHaveBeenCalledWith("log", true, null);
expect(mockLogger.warn).toHaveBeenCalledWith("warn", "multiple", "parameters");
expect(mockLogger.error).toHaveBeenCalledWith("error", { error: "object" });
});
});
});
18 changes: 18 additions & 0 deletions src/lib/core/Logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ILogger } from "$lib/types.js";

const stockLogger: ILogger = globalThis.console;

const noop = () => { };

const offLogger: ILogger = {
debug: noop,
log: noop,
warn: noop,
error: noop
};

export let logger: ILogger = stockLogger;

export function setLogger(newLogger: boolean | ILogger) {
logger = newLogger === true ? stockLogger : (newLogger === false ? offLogger : newLogger);
};
12 changes: 12 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { setLocation } from "./core/Location.js";
import { LocationFull } from "./core/LocationFull.js";
import { LocationLite } from "./core/LocationLite.svelte.js";
import { setLogger } from "./core/Logger.js";
import { routingOptions, type RoutingOptions } from "./core/options.js";
import { setTraceOptions, type TraceOptions } from "./core/trace.svelte.js";
import type { ILogger } from "./types.js";

/**
* Library's initialization options.
Expand All @@ -12,6 +14,15 @@ export type InitOptions = RoutingOptions & {
* Tracing options that generally should be off for production builds.
*/
trace?: TraceOptions;
/**
* Controls logging. If `true`, the default logger that logs to the console is used. If `false`, logging is
* turned off. If an object is provided, it is used as the logger.
*
* Logging is turned on by default.
*
* **TIP**: You can provide your own logger implementation to integrate with your application's logging system.
*/
logger?: boolean | ILogger;
}

/**
Expand All @@ -30,6 +41,7 @@ export type InitOptions = RoutingOptions & {
*/
export function init(options?: InitOptions): () => void {
setTraceOptions(options?.trace);
setLogger(options?.logger ?? true);
routingOptions.full = options?.full ?? routingOptions.full;
routingOptions.hashMode = options?.hashMode ?? routingOptions.hashMode;
routingOptions.implicitMode = options?.implicitMode ?? routingOptions.implicitMode;
Expand Down
22 changes: 22 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,25 @@ export type ActiveState = {
* @returns `true` if the fallback content should be shown; `false` to prevent content from being shown.
*/
export type WhenPredicate = (routeStatus: Record<string, RouteStatus>, noMatches: boolean) => boolean;

/**
* Defines the shape of logger objects that can be given to this library during initialization.
*/
export interface ILogger {
/**
* See `console.debug()` for reference.
*/
debug: (...args: any[]) => void;
/**
* See `console.log()` for reference.
*/
log: (...args: any[]) => void;
/**
* See `console.warn()` for reference.
*/
warn: (...args: any[]) => void;
/**
* See `console.error()` for reference.
*/
error: (...args: any[]) => void;
};