-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'refs/remotes/origin/kasper/module-mocki…
…ng' into kasper/before-each # Conflicts: # code/lib/preview-api/src/modules/preview-web/render/StoryRender.ts
- Loading branch information
Showing
35 changed files
with
1,536 additions
and
376 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/* 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'; | ||
// 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'; | ||
|
||
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; | ||
} | ||
|
||
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((...args: [name: string] | [RequestCookie] | []) => { | ||
const all = Array.from(this._parsed); | ||
if (!args.length) { | ||
return all.map(([_, value]) => value); | ||
} | ||
|
||
const name = typeof args[0] === 'string' ? args[0] : args[0]?.name; | ||
return all.filter(([n]) => n === name).map(([_, value]) => value); | ||
}).mockName('cookies().getAll'); | ||
|
||
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('; '); | ||
} | ||
} | ||
|
||
let requestCookiesMock: RequestCookiesMock; | ||
|
||
export const cookies = fn(() => { | ||
if (!requestCookiesMock) { | ||
requestCookiesMock = new RequestCookiesMock(headers()); | ||
} | ||
return requestCookiesMock; | ||
}); | ||
|
||
const originalRestore = cookies.mockRestore.bind(null); | ||
|
||
// will be called automatically by the test loader | ||
cookies.mockRestore = () => { | ||
originalRestore(); | ||
headers.mockRestore(); | ||
requestCookiesMock = new RequestCookiesMock(headers()); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
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. | ||
export class HeadersAdapterMock extends Headers implements HeadersAdapter { | ||
private headers: IncomingHttpHeaders = {}; | ||
|
||
/** | ||
* 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; | ||
} | ||
|
||
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); | ||
|
||
return null; | ||
}).mockName('headers().get'); | ||
|
||
public has = fn((name: string): boolean => { | ||
return typeof this.headers[name] !== 'undefined'; | ||
}).mockName('headers().has'); | ||
|
||
public set = fn((name: string, value: string): void => { | ||
this.headers[name] = value; | ||
}).mockName('headers().set'); | ||
|
||
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'); | ||
|
||
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; | ||
|
||
yield [name, value]; | ||
} | ||
}.bind(this) | ||
).mockName('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'); | ||
|
||
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(); | ||
} | ||
} | ||
|
||
let headersAdapterMock: HeadersAdapterMock; | ||
|
||
export const headers = () => { | ||
if (!headersAdapterMock) headersAdapterMock = new HeadersAdapterMock(); | ||
return headersAdapterMock; | ||
}; | ||
|
||
// This fn is called by ./cookies to restore the headers in the right order | ||
headers.mockRestore = () => { | ||
headersAdapterMock = new HeadersAdapterMock(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './headers'; | ||
export * from './cookies'; |
Oops, something went wrong.