Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
test: add loader handler unit tests
  • Loading branch information
Varixo authored and wmertens committed Sep 21, 2025
commit b202c40041cf12e2d5ef9c6ad6298775c495f1f8
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { _serialize, type ValueOrPromise } from '@qwik.dev/core/internal';
import type {
ActionInternal,
JSONObject,
RequestEvent,
RequestHandler,
} from '../../../runtime/src/types';
import { runValidators } from './loader-handler';
import { getRequestActions, getRequestMode, type RequestEventInternal } from '../request-event';
import { measure, verifySerializable } from '../resolve-request-handlers';
import { IsQAction, QActionId } from '../user-response';
import { _serialize, _UNINITIALIZED, type ValueOrPromise } from '@qwik.dev/core/internal';
import { runValidators } from './validator-utils';

export function actionHandler(routeActions: ActionInternal[]): RequestHandler {
return async (requestEvent: RequestEvent) => {
Expand Down Expand Up @@ -58,7 +58,7 @@ export function actionHandler(routeActions: ActionInternal[]): RequestHandler {
await executeAction(action, actions, requestEv, isDev);

if (requestEv.request.headers.get('accept')?.includes('application/json')) {
// only return the action data if the client accepts json, otherwise return the html page
// only return the action data if the client accepts json, otherwise it will return the html page (for forms)
const data = await _serialize([actions[actionId]]);
requestEv.headers.set('Content-Type', 'application/json; charset=utf-8');
requestEv.send(200, data);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { describe, it, expect, vi, beforeEach, afterEach, Mock, type Mocked } from 'vitest';
import { actionHandler } from './action-handler';
import type { QRL } from 'packages/qwik/public';
import { afterEach, beforeEach, describe, expect, it, Mock, vi, type Mocked } from 'vitest';
import type { ActionInternal, ActionStore } from '../../../runtime/src/types';
import type { RequestEventInternal } from '../request-event';
import type { QwikSerializer } from '../types';
import { getRequestActions, getRequestMode } from '../request-event';
import { measure, verifySerializable } from '../resolve-request-handlers';
import { IsQAction, QActionId } from '../user-response';
import { RequestEvQwikSerializer } from '../request-event';
import type { QRL } from 'packages/qwik/public';
import { actionHandler } from './action-handler';
import { runValidators } from './validator-utils';
import { _serialize } from '@qwik.dev/core/internal';

// Mock dependencies
vi.mock('./loader-handler', () => ({
vi.mock('./validator-utils', () => ({
runValidators: vi.fn(),
}));

Expand All @@ -23,14 +25,32 @@ vi.mock('../request-event', () => ({
RequestEvQwikSerializer: Symbol('RequestEvQwikSerializer'),
}));

const { runValidators } = await import('./loader-handler');
const { measure, verifySerializable } = await import('../resolve-request-handlers');
const { getRequestActions, getRequestMode } = await import('../request-event');
function createMockAction(id: string, hash: string): Mocked<ActionInternal> {
const mockActionFunction = (): Mocked<ActionStore<unknown, unknown>> => ({
actionPath: `?action=${id}`,
isRunning: false,
status: undefined,
value: undefined,
formData: undefined,
submit: vi.fn() as any,
submitted: false,
});

return {
__brand: 'server_action' as const,
__id: id,
__qrl: {
call: vi.fn(),
getHash: vi.fn().mockReturnValue(hash),
} as unknown as Mocked<QRL<(form: any, event: any) => any>>,
__validators: [],
...mockActionFunction,
} as unknown as Mocked<ActionInternal>;
}

describe('actionHandler', () => {
let mockRequestEvent: Mocked<RequestEventInternal>;
let mockAction: Mocked<ActionInternal>;
let mockQwikSerializer: Mocked<QwikSerializer>;
let mockActions: Record<string, any>;
let consoleSpy: any;

Expand All @@ -41,42 +61,11 @@ describe('actionHandler', () => {
// Reset all mocks
vi.clearAllMocks();

// Mock console.warn
consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

// Create mock action
const mockActionFunction = (): Mocked<ActionStore<unknown, unknown>> => ({
actionPath: `?action=${mockActionId}`,
isRunning: false,
status: undefined,
value: undefined,
formData: undefined,
submit: vi.fn() as any,
submitted: false,
});
mockAction = createMockAction(mockActionId, mockActionHash);

mockAction = {
__brand: 'server_action' as const,
__id: mockActionId,
__qrl: {
call: vi.fn(),
getHash: vi.fn().mockReturnValue(mockActionHash),
} as unknown as Mocked<QRL<(form: any, event: any) => any>>,
__validators: [],
...mockActionFunction,
} as unknown as Mocked<ActionInternal>;

// Create mock serializer
mockQwikSerializer = {
_serialize: vi.fn(),
_deserialize: vi.fn(),
_verifySerializable: vi.fn(),
} as Mocked<QwikSerializer>;

// Create mock actions record
mockActions = {};

// Create mock request event
mockRequestEvent = {
sharedMap: new Map(),
headersSent: false,
Expand Down Expand Up @@ -123,9 +112,8 @@ describe('actionHandler', () => {
} as unknown as Mocked<RequestEventInternal>;

// Set up default mocks
(getRequestActions as Mock).mockReturnValue(mockActions);
(getRequestMode as Mock).mockReturnValue('dev');
mockRequestEvent[RequestEvQwikSerializer] = mockQwikSerializer;
vi.mocked(getRequestActions).mockReturnValue(mockActions);
vi.mocked(getRequestMode).mockReturnValue('dev');
});

afterEach(() => {
Expand Down Expand Up @@ -241,33 +229,30 @@ describe('actionHandler', () => {

const data = { test: 'data' };

(mockRequestEvent.parseBody as Mock).mockResolvedValue(data);
vi.mocked(mockRequestEvent.parseBody).mockResolvedValue(data);
(runValidators as Mock).mockResolvedValue({
success: true,
data,
});

(mockQwikSerializer._serialize as Mock).mockResolvedValue(data);

const handler = actionHandler([mockAction]);

await handler(mockRequestEvent);

expect(mockRequestEvent.send).toBeCalledWith(200, data);
expect(mockRequestEvent.send).toBeCalledWith(200, await _serialize([undefined]));
});
});

describe('when action is found and executed successfully', () => {
beforeEach(() => {
mockRequestEvent.sharedMap.set(IsQAction, true);
mockRequestEvent.sharedMap.set(QActionId, mockActionId);
(mockRequestEvent.parseBody as Mock).mockResolvedValue({ test: 'data' });
(runValidators as Mock).mockResolvedValue({
vi.mocked(mockRequestEvent.parseBody).mockResolvedValue({ test: 'data' });
vi.mocked(runValidators).mockResolvedValue({
success: true,
data: { test: 'data' },
});
(mockAction.__qrl.call as Mock).mockResolvedValue({ result: 'success' });
(mockQwikSerializer._serialize as Mock).mockResolvedValue('serialized-data');
vi.mocked(mockAction.__qrl.call).mockResolvedValue({ result: 'success' });
});

it('should execute action and return serialized data', async () => {
Expand All @@ -286,29 +271,29 @@ describe('actionHandler', () => {
{ test: 'data' },
mockRequestEvent
);
expect(mockQwikSerializer._serialize).toHaveBeenCalledWith([{ result: 'success' }]);
expect(mockRequestEvent.headers.set).toHaveBeenCalledWith(
'Content-Type',
'application/json; charset=utf-8'
);
expect(mockRequestEvent.send).toHaveBeenCalledWith(200, 'serialized-data');
expect(mockRequestEvent.send).toHaveBeenCalledWith(
200,
await _serialize([{ result: 'success' }])
);
});

it('should measure execution time in dev mode', async () => {
vi.mocked(getRequestMode).mockReturnValue('dev');

const handler = actionHandler([mockAction]);

await handler(mockRequestEvent);

expect(measure).toHaveBeenCalledWith(mockRequestEvent, mockActionHash, expect.any(Function));
expect(verifySerializable).toHaveBeenCalledWith(
mockQwikSerializer,
{ result: 'success' },
mockAction.__qrl
);
expect(verifySerializable).toHaveBeenCalledWith({ result: 'success' }, mockAction.__qrl);
});

it('should not measure execution time in production mode', async () => {
(getRequestMode as Mock).mockReturnValue('prod');
vi.mocked(getRequestMode).mockReturnValue('server');

const handler = actionHandler([mockAction]);

Expand All @@ -325,7 +310,6 @@ describe('actionHandler', () => {

await handler(mockRequestEvent);

expect(mockQwikSerializer._serialize).not.toHaveBeenCalled();
expect(mockRequestEvent.send).not.toHaveBeenCalled();
});
});
Expand Down Expand Up @@ -473,7 +457,6 @@ describe('actionHandler', () => {
data: { test: 'data' },
});
(mockAction.__qrl.call as Mock).mockResolvedValue({ result: 'success' });
(mockQwikSerializer._serialize as Mock).mockResolvedValue('serialized-data');

const handler = actionHandler([mockAction]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import qwikRouterConfig from '@qwik-router-config';
import { _serialize, _UNINITIALIZED } from '@qwik.dev/core/internal';
import type {
DataValidator,
LoaderInternal,
RequestHandler,
ValidatorReturn,
} from '../../../runtime/src/types';
import type { LoaderInternal, RequestHandler } from '../../../runtime/src/types';
import { getPathnameForDynamicRoute } from '../../../utils/pathname';
import {
getRequestLoaders,
Expand All @@ -16,6 +11,7 @@ import {
import { measure, verifySerializable } from '../resolve-request-handlers';
import type { RequestEvent } from '../types';
import { IsQLoader, IsQLoaderData, QLoaderId } from '../user-response';
import { runValidators } from './validator-utils';

export function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandler {
return async (requestEvent: RequestEvent) => {
Expand Down Expand Up @@ -132,6 +128,7 @@ export async function executeLoader(
isDev: boolean
) {
const loaderId = loader.__id;

loaders[loaderId] = runValidators(
requestEv,
loader.__validators,
Expand Down Expand Up @@ -166,32 +163,3 @@ export async function executeLoader(
loadersSerializationStrategy.set(loaderId, loader.__serializationStrategy);
return loaders[loaderId];
}

export async function runValidators(
requestEv: RequestEvent,
validators: DataValidator[] | undefined,
data: unknown,
isDev: boolean
) {
let lastResult: ValidatorReturn = {
success: true,
data,
};
if (validators) {
for (const validator of validators) {
if (isDev) {
lastResult = await measure(requestEv, `validator$`, () =>
validator.validate(requestEv, data)
);
} else {
lastResult = await validator.validate(requestEv, data);
}
if (!lastResult.success) {
return lastResult;
} else {
data = lastResult.data;
}
}
}
return lastResult;
}
Loading