Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next: Use <<package>>::<<import>> naming convention for mock names #26853

Merged
merged 11 commits into from
Apr 19, 2024
4 changes: 2 additions & 2 deletions code/frameworks/nextjs/src/export-mocks/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cac
import { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store';

// mock utilities/overrides (as of Next v14.2.0)
const revalidatePath = fn().mockName('revalidatePath');
const revalidateTag = fn().mockName('revalidateTag');
const revalidatePath = fn().mockName('next/cache::revalidatePath');
const revalidateTag = fn().mockName('next/cache::revalidateTag');

const cacheExports = {
unstable_cache,
Expand Down
112 changes: 9 additions & 103 deletions code/frameworks/nextjs/src/export-mocks/headers/cookies.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,22 @@
/* eslint-disable no-underscore-dangle */
import { fn } from '@storybook/test';
import type { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies';
import {
parseCookie,
stringifyCookie,
type RequestCookie,
} from 'next/dist/compiled/@edge-runtime/cookies';
import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies';
// We need this import to be a singleton, and because it's used in multiple entrypoints
// both in ESM and CJS, importing it via the package name instead of having a local import
// is the only way to achieve it actually being a singleton
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore we must ignore types here as during compilation they are not generated yet
import { headers, type HeadersStore } from '@storybook/nextjs/headers.mock';
import { headers } from '@storybook/nextjs/headers.mock';

const stringifyCookies = (map: Map<string, RequestCookie>) => {
return Array.from(map)
.map(([_, v]) => stringifyCookie(v).replace(/; /, ''))
.join('; ');
};

// Mostly copied from https://github.com/vercel/edge-runtime/blob/c25e2ded39104e2a3be82efc08baf8dc8fb436b3/packages/cookies/src/request-cookies.ts#L7
class RequestCookiesMock implements RequestCookies {
/** @internal */
private readonly _headers: HeadersStore;

_parsed: Map<string, RequestCookie> = new Map();

constructor(requestHeaders: HeadersStore) {
this._headers = requestHeaders;
const header = requestHeaders?.get('cookie');
if (header) {
const parsed = parseCookie(header);
for (const [name, value] of parsed) {
this._parsed.set(name, { name, value });
}
}
}

[Symbol.iterator]() {
return this._parsed[Symbol.iterator]();
}

get size(): number {
return this._parsed.size;
}
class RequestCookiesMock extends RequestCookies {
get = fn(super.get.bind(this)).mockName('next/headers::cookies().get');

get = fn((...args: [name: string] | [RequestCookie]) => {
const name = typeof args[0] === 'string' ? args[0] : args[0].name;
return this._parsed.get(name);
}).mockName('cookies().get');
getAll = fn(super.getAll.bind(this)).mockName('next/headers::cookies().getAll');

getAll = fn((...args: [name: string] | [RequestCookie] | []) => {
const all = Array.from(this._parsed);
if (!args.length) {
return all.map(([_, value]) => value);
}
has = fn(super.has.bind(this)).mockName('next/headers::cookies().has');

const name = typeof args[0] === 'string' ? args[0] : args[0]?.name;
return all.filter(([n]) => n === name).map(([_, value]) => value);
}).mockName('cookies().getAll');
set = fn(super.set.bind(this)).mockName('next/headers::cookies().set');

has = fn((name: string) => {
return this._parsed.has(name);
}).mockName('cookies().has');

set = fn((...args: [key: string, value: string] | [options: RequestCookie]): this => {
const [name, value] = args.length === 1 ? [args[0].name, args[0].value] : args;

const map = this._parsed;
map.set(name, { name, value });

this._headers.set('cookie', stringifyCookies(map));
return this;
}).mockName('cookies().set');

/**
* Delete the cookies matching the passed name or names in the request.
*/
delete = fn(
(
/** Name or names of the cookies to be deleted */
names: string | string[]
): boolean | boolean[] => {
const map = this._parsed;
const result = !Array.isArray(names)
? map.delete(names)
: names.map((name) => map.delete(name));
this._headers.set('cookie', stringifyCookies(map));
return result;
}
).mockName('cookies().delete');

/**
* Delete all the cookies in the cookies in the request.
*/
clear = fn((): this => {
this.delete(Array.from(this._parsed.keys()));
return this;
}).mockName('cookies().clear');

/**
* Format the cookies in the request as a string for logging
*/
[Symbol.for('edge-runtime.inspect.custom')]() {
return `RequestCookies ${JSON.stringify(Object.fromEntries(this._parsed))}`;
}

toString() {
return [...this._parsed.values()]
.map((v) => `${v.name}=${encodeURIComponent(v.value)}`)
.join('; ');
}
delete = fn(super.delete.bind(this)).mockName('next/headers::cookies().delete');
}

let requestCookiesMock: RequestCookiesMock;
Expand All @@ -120,7 +26,7 @@ export const cookies = fn(() => {
requestCookiesMock = new RequestCookiesMock(headers());
}
return requestCookiesMock;
});
}).mockName('next/headers::cookies()');

const originalRestore = cookies.mockRestore.bind(null);

Expand Down
98 changes: 13 additions & 85 deletions code/frameworks/nextjs/src/export-mocks/headers/headers.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,29 @@
import { fn } from '@storybook/test';
import type { IncomingHttpHeaders } from 'http';
import type { HeadersAdapter } from 'next/dist/server/web/spec-extension/adapters/headers';

// Mostly copied from https://github.com/vercel/next.js/blob/763b9a660433ec5278a10e59d7ae89d4010ba212/packages/next/src/server/web/spec-extension/adapters/headers.ts#L20
// @ts-expect-error unfortunately the headers property is private (and not protected) in HeadersAdapter
// and we can't access it so we need to redefine it, but that clashes with the type, hence the ts-expect-error comment.
class HeadersAdapterMock extends Headers implements HeadersAdapter {
private headers: IncomingHttpHeaders = {};
import { HeadersAdapter } from 'next/dist/server/web/spec-extension/adapters/headers';

/**
* Merges a header value into a string. This stores multiple values as an
* array, so we need to merge them into a string.
*
* @param value a header value
* @returns a merged header value (a string)
*/
private merge(value: string | string[]): string {
if (Array.isArray(value)) return value.join(', ');

return value;
class HeadersAdapterMock extends HeadersAdapter {
constructor() {
super({});
}

public append = fn((name: string, value: string): void => {
const existing = this.headers[name];
if (typeof existing === 'string') {
this.headers[name] = [existing, value];
} else if (Array.isArray(existing)) {
existing.push(value);
} else {
this.headers[name] = value;
}
}).mockName('headers().append');

public delete = fn((name: string) => {
delete this.headers[name];
}).mockName('headers().delete');

public get = fn((name: string): string | null => {
const value = this.headers[name];
if (typeof value !== 'undefined') return this.merge(value);
append = fn(super.append.bind(this)).mockName('next/headers::headers().append');

return null;
}).mockName('headers().get');
delete = fn(super.delete.bind(this)).mockName('next/headers::headers().delete');

public has = fn((name: string): boolean => {
return typeof this.headers[name] !== 'undefined';
}).mockName('headers().has');
get = fn(super.get.bind(this)).mockName('next/headers::headers().get');

public set = fn((name: string, value: string): void => {
this.headers[name] = value;
}).mockName('headers().set');
has = fn(super.has.bind(this)).mockName('next/headers::headers().has');

public forEach = fn(
(callbackfn: (value: string, name: string, parent: Headers) => void, thisArg?: any): void => {
for (const [name, value] of this.entries()) {
callbackfn.call(thisArg, value, name, this);
}
}
).mockName('headers().forEach');
set = fn(super.set.bind(this)).mockName('next/headers::headers().set');

public entries = fn(
function* (this: HeadersAdapterMock): IterableIterator<[string, string]> {
for (const key of Object.keys(this.headers)) {
const name = key.toLowerCase();
// We assert here that this is a string because we got it from the
// Object.keys() call above.
const value = this.get(name) as string;
forEach = fn(super.forEach.bind(this)).mockName('next/headers::headers().forEach');

yield [name, value];
}
}.bind(this)
).mockName('headers().entries');
entries = fn(super.entries.bind(this)).mockName('next/headers::headers().entries');

public keys = fn(
function* (this: HeadersAdapterMock): IterableIterator<string> {
for (const key of Object.keys(this.headers)) {
const name = key.toLowerCase();
yield name;
}
}.bind(this)
).mockName('headers().keys');
keys = fn(super.keys.bind(this)).mockName('next/headers::headers().keys');

public values = fn(
function* (this: HeadersAdapterMock): IterableIterator<string> {
for (const key of Object.keys(this.headers)) {
// We assert here that this is a string because we got it from the
// Object.keys() call above.
const value = this.get(key) as string;

yield value;
}
}.bind(this)
).mockName('headers().values');

public [Symbol.iterator](): IterableIterator<[string, string]> {
return this.entries();
}
values = fn(super.values.bind(this)).mockName('next/headers::headers().values');
}

let headersAdapterMock: HeadersAdapterMock;
Expand Down
59 changes: 26 additions & 33 deletions code/frameworks/nextjs/src/export-mocks/navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ let navigationAPI: {
* @ignore
* @internal
* */
const createNavigation = (overrides: any) => {
export const createNavigation = (overrides: any) => {
const navigationActions = {
push: fn().mockName('useRouter().push'),
replace: fn().mockName('useRouter().replace'),
forward: fn().mockName('useRouter().forward'),
back: fn().mockName('useRouter().back'),
prefetch: fn().mockName('useRouter().prefetch'),
refresh: fn().mockName('useRouter().refresh'),
push: fn().mockName('next/navigation::useRouter().push'),
replace: fn().mockName('next/navigation::useRouter().replace'),
forward: fn().mockName('next/navigation::useRouter().forward'),
back: fn().mockName('next/navigation::useRouter().back'),
prefetch: fn().mockName('next/navigation::useRouter().prefetch'),
refresh: fn().mockName('next/navigation::useRouter().refresh'),
};

if (overrides) {
Expand All @@ -42,7 +42,7 @@ const createNavigation = (overrides: any) => {
return navigationAPI;
};

const getRouter = () => {
export const getRouter = () => {
if (!navigationAPI) {
throw new NextjsRouterMocksNotAvailable({
importType: 'next/navigation',
Expand All @@ -56,41 +56,34 @@ const getRouter = () => {
export * from 'next/dist/client/components/navigation';

// mock utilities/overrides (as of Next v14.2.0)
const redirect = fn().mockName('redirect');
export const redirect = fn().mockName('next/navigation::redirect');

// passthrough mocks - keep original implementation but allow for spying
const useSearchParams = fn(originalNavigation.useSearchParams).mockName('useSearchParams');
const usePathname = fn(originalNavigation.usePathname).mockName('usePathname');
const useSelectedLayoutSegment = fn(originalNavigation.useSelectedLayoutSegment).mockName(
export const useSearchParams = fn(originalNavigation.useSearchParams).mockName(
'next/navigation::useSearchParams'
);
export const usePathname = fn(originalNavigation.usePathname).mockName(
'next/navigation::usePathname'
);
export const useSelectedLayoutSegment = fn(originalNavigation.useSelectedLayoutSegment).mockName(
'useSelectedLayoutSegment'
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
);
const useSelectedLayoutSegments = fn(originalNavigation.useSelectedLayoutSegments).mockName(
export const useSelectedLayoutSegments = fn(originalNavigation.useSelectedLayoutSegments).mockName(
'useSelectedLayoutSegments'
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
);
const useRouter = fn(originalNavigation.useRouter).mockName('useRouter');
const useServerInsertedHTML = fn(originalNavigation.useServerInsertedHTML).mockName(
export const useRouter = fn(originalNavigation.useRouter).mockName('next/navigation::useRouter');
export const useServerInsertedHTML = fn(originalNavigation.useServerInsertedHTML).mockName(
'useServerInsertedHTML'
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
);
const notFound = fn(originalNavigation.notFound).mockName('notFound');
const permanentRedirect = fn(originalNavigation.permanentRedirect).mockName('permanentRedirect');
export const notFound = fn(originalNavigation.notFound).mockName('next/navigation::notFound');
export const permanentRedirect = fn(originalNavigation.permanentRedirect).mockName(
'permanentRedirect'
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
);

// Params, not exported by Next.js, is manually declared to avoid inference issues.
interface Params {
[key: string]: string | string[];
}
const useParams = fn<[], Params>(originalNavigation.useParams).mockName('useParams');

export {
createNavigation,
getRouter,
redirect,
useSearchParams,
usePathname,
useSelectedLayoutSegment,
useSelectedLayoutSegments,
useParams,
useRouter,
useServerInsertedHTML,
notFound,
permanentRedirect,
};
export const useParams = fn<[], Params>(originalNavigation.useParams).mockName(
'next/navigation::useParams'
);
Loading