aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--cypress/e2e/example-refresh.cy.ts32
-rw-r--r--docs/.vitepress/components/api-docs/format.ts14
-rw-r--r--docs/.vitepress/components/api-docs/method.ts1
-rw-r--r--docs/.vitepress/components/api-docs/method.vue120
-rw-r--r--docs/.vitepress/components/api-docs/refresh-button.vue69
-rw-r--r--docs/.vitepress/components/api-docs/refresh.svg1
-rw-r--r--docs/.vitepress/config.ts1
-rw-r--r--docs/api/.gitignore10
-rw-r--r--eslint.config.ts1
-rw-r--r--scripts/apidocs/output/page.ts80
-rw-r--r--test/docs/__snapshots__/format.spec.ts.snap19
-rw-r--r--test/docs/format.spec.ts69
-rw-r--r--test/scripts/apidocs/__snapshots__/page.spec.ts.snap97
-rw-r--r--test/scripts/apidocs/page.spec.ts114
-rw-r--r--tsconfig.json2
16 files changed, 618 insertions, 17 deletions
diff --git a/.gitignore b/.gitignore
index a26870e4..df1219d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,6 +80,11 @@ versions.json
/dist
/docs/.vitepress/cache
/docs/.vitepress/dist
+/docs/api/*.ts
+!/docs/api/api-types.ts
+/docs/api/*.md
+!/docs/api/index.md
+/docs/api/api-search-index.json
/docs/public/api-diff-index.json
# Faker
diff --git a/cypress/e2e/example-refresh.cy.ts b/cypress/e2e/example-refresh.cy.ts
new file mode 100644
index 00000000..df792eba
--- /dev/null
+++ b/cypress/e2e/example-refresh.cy.ts
@@ -0,0 +1,32 @@
+describe('example-refresh', () => {
+ it('should refresh the example', () => {
+ // given
+ cy.visit('/api/faker.html#constructor');
+ cy.get('.refresh').first().as('refresh');
+ cy.get('@refresh').next().find('code').as('codeBlock');
+ cy.get('@codeBlock').then(($el) => {
+ const originalCodeText = $el.text();
+
+ cy.get('@refresh')
+ .click()
+ .should('not.be.disabled') // stays disabled on error
+ .then(() => {
+ cy.get('@codeBlock').then(($el) => {
+ const newCodeText = $el.text();
+ expect(newCodeText).not.to.equal(originalCodeText);
+
+ cy.get('@refresh')
+ .click()
+ .should('not.be.disabled') // stays disabled on error
+ .then(() => {
+ cy.get('@codeBlock').then(($el) => {
+ const newCodeText2 = $el.text();
+ expect(newCodeText2).not.to.equal(originalCodeText);
+ expect(newCodeText2).not.to.equal(newCodeText);
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/docs/.vitepress/components/api-docs/format.ts b/docs/.vitepress/components/api-docs/format.ts
new file mode 100644
index 00000000..34de1e0c
--- /dev/null
+++ b/docs/.vitepress/components/api-docs/format.ts
@@ -0,0 +1,14 @@
+export function formatResult(result: unknown): string {
+ return result === undefined
+ ? 'undefined'
+ : typeof result === 'bigint'
+ ? `${result}n`
+ : JSON.stringify(result, undefined, 2)
+ .replaceAll('\\r', '')
+ .replaceAll('<', '&lt;')
+ .replaceAll(
+ /(^ *|: )"([^'\n]*?)"(?=,?$|: )/gm,
+ (_, p1, p2) => `${p1}'${p2.replace(/\\"/g, '"')}'`
+ )
+ .replaceAll(/\n */g, ' ');
+}
diff --git a/docs/.vitepress/components/api-docs/method.ts b/docs/.vitepress/components/api-docs/method.ts
index 4da480b8..91f99d4e 100644
--- a/docs/.vitepress/components/api-docs/method.ts
+++ b/docs/.vitepress/components/api-docs/method.ts
@@ -8,6 +8,7 @@ export interface ApiDocsMethod {
readonly throws: string | undefined; // HTML
readonly signature: string; // HTML
readonly examples: string; // HTML
+ readonly refresh: (() => Promise<unknown[]>) | undefined;
readonly seeAlsos: string[];
readonly sourcePath: string; // URL-Suffix
}
diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue
index 83a4100c..37b44748 100644
--- a/docs/.vitepress/components/api-docs/method.vue
+++ b/docs/.vitepress/components/api-docs/method.vue
@@ -1,8 +1,11 @@
<script setup lang="ts">
+import { computed, ref, useTemplateRef } from 'vue';
import { sourceBaseUrl } from '../../../api/source-base-url';
import { slugify } from '../../shared/utils/slugify';
+import { formatResult } from './format';
import type { ApiDocsMethod } from './method';
import MethodParameters from './method-parameters.vue';
+import RefreshButton from './refresh-button.vue';
const { method } = defineProps<{ method: ApiDocsMethod }>();
const {
@@ -14,10 +17,113 @@ const {
throws,
signature,
examples,
+ refresh,
seeAlsos,
sourcePath,
} = method;
+const code = useTemplateRef('code');
+const codeBlock = computed(() => code.value?.querySelector('div pre code'));
+const codeLines = ref<Element[]>();
+
+function initRefresh(): Element[] {
+ if (codeBlock.value == null) {
+ return [];
+ }
+ const domLines = codeBlock.value.querySelectorAll('.line');
+ let lineIndex = 0;
+ const result: Element[] = [];
+ while (lineIndex < domLines.length) {
+ // Skip empty and preparatory lines (no '^faker.' invocation)
+ if (
+ domLines[lineIndex]?.children.length === 0 ||
+ !/^\w*faker\w*\./i.test(domLines[lineIndex]?.textContent ?? '')
+ ) {
+ lineIndex++;
+ continue;
+ }
+
+ // Skip to end of the invocation (if multiline)
+ while (
+ domLines[lineIndex] != null &&
+ !/^([^ ].*)?\)(\.\w+)?;? ?(\/\/|$)/.test(
+ domLines[lineIndex]?.textContent ?? ''
+ )
+ ) {
+ lineIndex++;
+ }
+
+ if (lineIndex >= domLines.length) {
+ break;
+ }
+
+ const domLine = domLines[lineIndex];
+ result.push(domLine);
+ lineIndex++;
+
+ // Purge old results
+ if (domLine.lastElementChild?.textContent?.startsWith('//')) {
+ // Inline comments
+ domLine.lastElementChild.remove();
+ } else {
+ // Multiline comments
+ while (domLines[lineIndex]?.children[0]?.textContent?.startsWith('//')) {
+ domLines[lineIndex].previousSibling?.remove(); // newline
+ domLines[lineIndex].remove(); // comment
+ lineIndex++;
+ }
+ }
+
+ // Add space between invocation and comment (if missing)
+ const lastElementChild = domLine.lastElementChild;
+ if (
+ lastElementChild != null &&
+ !lastElementChild.textContent?.endsWith(' ')
+ ) {
+ lastElementChild.textContent += ' ';
+ }
+ }
+
+ return result;
+}
+
+async function onRefresh(): Promise<void> {
+ if (refresh != null && codeBlock.value != null) {
+ codeLines.value ??= initRefresh();
+
+ const results = await refresh();
+
+ // Remove old comments
+ codeBlock.value
+ .querySelectorAll('.comment-delete-marker')
+ .forEach((el) => el.remove());
+
+ // Insert new comments
+ for (let i = 0; i < results.length; i++) {
+ const result = results[i];
+ const domLine = codeLines.value[i];
+ const prettyResult = formatResult(result);
+ const resultLines = prettyResult.split('\\n');
+
+ if (resultLines.length === 1) {
+ domLine.insertAdjacentHTML('beforeend', newCommentSpan(resultLines[0]));
+ } else {
+ for (const line of resultLines.reverse()) {
+ domLine.insertAdjacentHTML('afterend', newCommentLine(line));
+ }
+ }
+ }
+ }
+}
+
+function newCommentLine(content: string): string {
+ return `<span class="line comment-delete-marker">\n${newCommentSpan(content)}</span>`;
+}
+
+function newCommentSpan(content: string): string {
+ return `<span class="comment-delete-marker" style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ${content}</span>`;
+}
+
function seeAlsoToUrl(see: string): string {
const [, module, methodName] = see.replace(/\(.*/, '').split('\.');
@@ -51,8 +157,14 @@ function seeAlsoToUrl(see: string): string {
<div v-html="signature" />
- <h3>Examples</h3>
- <div v-html="examples" />
+ <h3 class="inline">Examples</h3>
+ <RefreshButton
+ class="refresh"
+ v-if="refresh != null"
+ style="margin-left: 0.5em"
+ :refresh="onRefresh"
+ />
+ <div ref="code" v-html="examples" />
<div v-if="seeAlsos.length > 0">
<h3>See Also</h3>
@@ -107,4 +219,8 @@ svg.source-link-icon {
display: inline;
margin-left: 0.3em;
}
+
+h3.inline {
+ display: inline-block;
+}
</style>
diff --git a/docs/.vitepress/components/api-docs/refresh-button.vue b/docs/.vitepress/components/api-docs/refresh-button.vue
new file mode 100644
index 00000000..c2210eab
--- /dev/null
+++ b/docs/.vitepress/components/api-docs/refresh-button.vue
@@ -0,0 +1,69 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+
+// This should probably use emit instead, but emit cannot be awaited
+const { refresh } = defineProps<{ refresh: () => Promise<void> }>();
+
+const spinning = ref(false);
+
+async function onRefresh() {
+ spinning.value = true;
+ await Promise.all([refresh(), delay(100)]);
+ spinning.value = false;
+}
+
+// Extra delay to make the spinning effect more visible
+// Some examples barely/don't change, so the spinning is the only visible effect
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+</script>
+
+<template>
+ <button
+ class="refresh"
+ title="Refresh Examples"
+ :disabled="spinning"
+ @click="onRefresh"
+ >
+ <div :class="{ spinning: spinning }" />
+ </button>
+</template>
+
+<style scoped>
+button.refresh {
+ border: 1px solid var(--vp-code-copy-code-border-color);
+ border-radius: 4px;
+ width: 40px;
+ height: 40px;
+ font-size: 25px;
+ vertical-align: middle;
+}
+
+button.refresh div {
+ background-image: url('refresh.svg');
+ background-position: 50%;
+ background-size: 20px;
+ background-repeat: no-repeat;
+ width: 100%;
+ height: 100%;
+}
+
+button.refresh:hover {
+ background-color: var(--vp-code-copy-code-bg);
+ opacity: 1;
+}
+
+div.spinning {
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+</style>
diff --git a/docs/.vitepress/components/api-docs/refresh.svg b/docs/.vitepress/components/api-docs/refresh.svg
new file mode 100644
index 00000000..8320a2b2
--- /dev/null
+++ b/docs/.vitepress/components/api-docs/refresh.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M20 12a8 8 0 0 1-6.44 7.85 8 8 0 0 1-8.95-4.79 8 8 0 0 1 2.94-9.71 8 8 0 0 1 10.1 1" style="fill:none;stroke:rgba(128,128,128,1);stroke-width:2"/><path d="m19.65 4.25-4.04 4.03 5.5 1.47z" style="fill:rgba(128,128,128,1)"/></svg> \ No newline at end of file
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 746803ad..4fcc469e 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -133,6 +133,7 @@ async function enableFaker() {
e.g. 'faker.food.description()' or 'fakerZH_CN.person.firstName()'
For other languages please refer to https://fakerjs.dev/guide/localization.html#available-locales
For a full list of all methods please refer to https://fakerjs.dev/api/\`, logStyle);
+ enableFaker = () => imported; // Init only once
return imported;
}
`,
diff --git a/docs/api/.gitignore b/docs/api/.gitignore
deleted file mode 100644
index 47b11a83..00000000
--- a/docs/api/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Markdown
-*.md
-!index.md
-
-# TypeScript
-*.ts
-!api-types.ts
-
-# JSON
-*.json
diff --git a/eslint.config.ts b/eslint.config.ts
index 812c8a92..8c980b2c 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -24,6 +24,7 @@ const config: ReturnType<typeof tseslint.config> = tseslint.config(
'.github/workflows/commentCodeGeneration.ts',
'.prettierrc.js',
'docs/.vitepress/components/shims.d.ts',
+ 'docs/.vitepress/components/api-docs/format.ts',
'docs/.vitepress/shared/utils/slugify.ts',
'docs/.vitepress/theme/index.ts',
'eslint.config.js',
diff --git a/scripts/apidocs/output/page.ts b/scripts/apidocs/output/page.ts
index c00ba4d2..a9570309 100644
--- a/scripts/apidocs/output/page.ts
+++ b/scripts/apidocs/output/page.ts
@@ -33,7 +33,7 @@ export async function writePages(pages: RawApiDocsPage[]): Promise<void> {
async function writePage(page: RawApiDocsPage): Promise<void> {
try {
await writePageMarkdown(page);
- await writePageJsonData(page);
+ await writePageData(page);
} catch (error) {
throw new Error(`Error writing page ${page.title}`, { cause: error });
}
@@ -51,7 +51,7 @@ async function writePageMarkdown(page: RawApiDocsPage): Promise<void> {
let content = `
<script setup>
import ApiDocsMethod from '../.vitepress/components/api-docs/method.vue';
- import ${camelTitle} from './${camelTitle}.json';
+ import ${camelTitle} from './${camelTitle}.ts';
</script>
<!-- This file is automatically generated. -->
@@ -98,16 +98,33 @@ async function writePageMarkdown(page: RawApiDocsPage): Promise<void> {
*
* @param page The page to write.
*/
-async function writePageJsonData(page: RawApiDocsPage): Promise<void> {
+async function writePageData(page: RawApiDocsPage): Promise<void> {
const { camelTitle, methods } = page;
const pageData: Record<string, ApiDocsMethod> = Object.fromEntries(
await Promise.all(
methods.map(async (method) => [method.name, await toMethodData(method)])
)
);
- const content = JSON.stringify(pageData, null, 2);
- writeFileSync(resolve(FILE_PATH_API_DOCS, `${camelTitle}.json`), content);
+ const refreshFunctions: Record<string, string> = Object.fromEntries(
+ await Promise.all(
+ methods.map(async (method) => [
+ method.name,
+ await toRefreshFunction(method),
+ ])
+ )
+ );
+
+ const content =
+ `export default ${JSON.stringify(pageData, undefined, 2)}`.replaceAll(
+ /"refresh-([^"-]+)-placeholder"/g,
+ (_, name) => refreshFunctions[name]
+ );
+
+ writeFileSync(
+ resolve(FILE_PATH_API_DOCS, `${camelTitle}.ts`),
+ await formatTypescript(content)
+ );
}
const defaultCommentRegex = /\s+Defaults to `([^`]+)`\..*/;
@@ -130,6 +147,12 @@ async function toMethodData(method: RawApiDocsMethod): Promise<ApiDocsMethod> {
let formattedSignature = await formatTypescript(signature);
formattedSignature = formattedSignature.trim();
+ // eslint-disable-next-line @typescript-eslint/require-await
+ const refresh = async () => ['refresh', name, 'placeholder'];
+ // This is a placeholder to be replaced by the actual refresh function code
+ // If we put the actual code here, it would be a string and not executable
+ refresh.toJSON = () => `refresh-${name}-placeholder`;
+
/* Target order, omitted to improve diff to old files
return {
name,
@@ -167,6 +190,7 @@ async function toMethodData(method: RawApiDocsMethod): Promise<ApiDocsMethod> {
returns: returns.text,
signature: codeToHtml(formattedSignature),
examples: codeToHtml(examples.join('\n')),
+ refresh,
deprecated: mdToHtml(deprecated),
seeAlsos: seeAlsos.map((seeAlso) => mdToHtml(seeAlso, true)),
};
@@ -175,3 +199,49 @@ async function toMethodData(method: RawApiDocsMethod): Promise<ApiDocsMethod> {
export function extractSummaryDefault(description: string): string | undefined {
return defaultCommentRegex.exec(description)?.[1];
}
+
+export async function toRefreshFunction(
+ method: RawApiDocsMethod
+): Promise<string> {
+ const { name, signatures } = method;
+ const signatureData = required(signatures.at(-1), 'method signature');
+ const { examples } = signatureData;
+
+ const exampleCode = examples.join('\n');
+ if (!/^\w*faker\w*\./im.test(exampleCode)) {
+ // No recordable faker calls in examples
+ return 'undefined';
+ }
+
+ const exampleLines = exampleCode
+ .replaceAll(/ ?\/\/.*$/gm, '') // Remove comments
+ .replaceAll(/^import .*$/gm, '') // Remove imports
+ .replaceAll(
+ // record results of faker calls
+ /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?);?$/gim,
+ `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n`
+ );
+
+ const fullMethod = `async (): Promise<unknown[]> => {
+await enableFaker();
+faker.seed();
+faker.setDefaultRefDate();
+const result: unknown[] = [];
+
+${exampleLines}
+
+return result;
+}`;
+ try {
+ const formattedMethod = await formatTypescript(fullMethod);
+ return formattedMethod.replace(/;\s+$/, ''); // Remove trailing semicolon
+ } catch (error: unknown) {
+ console.error(
+ 'Failed to format refresh function for',
+ name,
+ fullMethod,
+ error
+ );
+ return 'undefined';
+ }
+}
diff --git a/test/docs/__snapshots__/format.spec.ts.snap b/test/docs/__snapshots__/format.spec.ts.snap
new file mode 100644
index 00000000..36679523
--- /dev/null
+++ b/test/docs/__snapshots__/format.spec.ts.snap
@@ -0,0 +1,19 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`formatResult > should format Date 1`] = `"'2025-01-01T00:00:00.000Z'"`;
+
+exports[`formatResult > should format array 1`] = `"[ 1, '2' ]"`;
+
+exports[`formatResult > should format bigint 1`] = `"135464154865415n"`;
+
+exports[`formatResult > should format number 1`] = `"123"`;
+
+exports[`formatResult > should format object 1`] = `"{ 'a': 1, 'b': '2' }"`;
+
+exports[`formatResult > should format string 1`] = `"'a simple string'"`;
+
+exports[`formatResult > should format string with new lines 1`] = `"'string\\nwith\\nnew\\nlines'"`;
+
+exports[`formatResult > should format string with special characters 1`] = `"'string with "special" characters'"`;
+
+exports[`formatResult > should format undefined 1`] = `"undefined"`;
diff --git a/test/docs/format.spec.ts b/test/docs/format.spec.ts
new file mode 100644
index 00000000..bc4a0d66
--- /dev/null
+++ b/test/docs/format.spec.ts
@@ -0,0 +1,69 @@
+import { describe, expect, it } from 'vitest';
+import { formatResult } from '../../docs/.vitepress/components/api-docs/format';
+
+describe('formatResult', () => {
+ it('should format undefined', () => {
+ const value = undefined;
+ const actual = formatResult(value);
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toBe('undefined');
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should format bigint', () => {
+ const actual = formatResult(135464154865415n);
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should format object', () => {
+ const actual = formatResult({ a: 1, b: '2' });
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should format array', () => {
+ const actual = formatResult([1, '2']);
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should format string', () => {
+ const actual = formatResult('a simple string');
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should format string with special characters', () => {
+ const actual = formatResult('string with "special" characters');
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should format string with new lines', () => {
+ const actual = formatResult('string\nwith\nnew\nlines');
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should format number', () => {
+ const actual = formatResult(123);
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toMatchSnapshot();
+ });
+
+ it('should format Date', () => {
+ const actual = formatResult(new Date(Date.UTC(2025, 0, 1)));
+
+ expect(actual).toBeTypeOf('string');
+ expect(actual).toMatchSnapshot();
+ });
+});
diff --git a/test/scripts/apidocs/__snapshots__/page.spec.ts.snap b/test/scripts/apidocs/__snapshots__/page.spec.ts.snap
new file mode 100644
index 00000000..4824c101
--- /dev/null
+++ b/test/scripts/apidocs/__snapshots__/page.spec.ts.snap
@@ -0,0 +1,97 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`toRefreshFunction > should handle multiline calls 1`] = `
+"async (): Promise<unknown[]> => {
+ await enableFaker();
+ faker.seed();
+ faker.setDefaultRefDate();
+ const result: unknown[] = [];
+
+ try {
+ result.push(
+ faker.number.int({
+ min: 1,
+ max: 10,
+ })
+ );
+ } catch (error: unknown) {
+ result.push(error instanceof Error ? error.name : 'Error');
+ }
+
+ return result;
+}"
+`;
+
+exports[`toRefreshFunction > should handle multiple calls 1`] = `
+"async (): Promise<unknown[]> => {
+ await enableFaker();
+ faker.seed();
+ faker.setDefaultRefDate();
+ const result: unknown[] = [];
+
+ try {
+ result.push(faker.number.int());
+ } catch (error: unknown) {
+ result.push(error instanceof Error ? error.name : 'Error');
+ }
+
+ try {
+ result.push(faker.number.int());
+ } catch (error: unknown) {
+ result.push(error instanceof Error ? error.name : 'Error');
+ }
+
+ return result;
+}"
+`;
+
+exports[`toRefreshFunction > should handle properties after calls 1`] = `
+"async (): Promise<unknown[]> => {
+ await enableFaker();
+ faker.seed();
+ faker.setDefaultRefDate();
+ const result: unknown[] = [];
+
+ try {
+ result.push(faker.airline.airport().name);
+ } catch (error: unknown) {
+ result.push(error instanceof Error ? error.name : 'Error');
+ }
+
+ return result;
+}"
+`;
+
+exports[`toRefreshFunction > should handle single line calls with semicolon 1`] = `
+"async (): Promise<unknown[]> => {
+ await enableFaker();
+ faker.seed();
+ faker.setDefaultRefDate();
+ const result: unknown[] = [];
+
+ try {
+ result.push(faker.number.int());
+ } catch (error: unknown) {
+ result.push(error instanceof Error ? error.name : 'Error');
+ }
+
+ return result;
+}"
+`;
+
+exports[`toRefreshFunction > should handle single line calls without semicolon 1`] = `
+"async (): Promise<unknown[]> => {
+ await enableFaker();
+ faker.seed();
+ faker.setDefaultRefDate();
+ const result: unknown[] = [];
+
+ try {
+ result.push(faker.number.int());
+ } catch (error: unknown) {
+ result.push(error instanceof Error ? error.name : 'Error');
+ }
+
+ return result;
+}"
+`;
diff --git a/test/scripts/apidocs/page.spec.ts b/test/scripts/apidocs/page.spec.ts
new file mode 100644
index 00000000..00fd4da2
--- /dev/null
+++ b/test/scripts/apidocs/page.spec.ts
@@ -0,0 +1,114 @@
+import { describe, expect, it } from 'vitest';
+import { toRefreshFunction } from '../../../scripts/apidocs/output/page';
+import type { RawApiDocsMethod } from '../../../scripts/apidocs/processing/method';
+import type { RawApiDocsSignature } from '../../../scripts/apidocs/processing/signature';
+
+function newTestMethod(
+ signature: Partial<RawApiDocsSignature>
+): RawApiDocsMethod {
+ return {
+ name: 'test',
+ signatures: [
+ {
+ deprecated: 'deprecated',
+ description: 'description',
+ since: 'since',
+ parameters: [],
+ returns: {
+ type: 'simple',
+ text: 'returns',
+ },
+ throws: [],
+ signature: 'signature',
+ examples: [],
+ seeAlsos: [],
+ ...signature,
+ },
+ ],
+ source: {
+ filePath: 'test/page.spec.ts',
+ line: 1,
+ column: 1,
+ },
+ };
+}
+
+describe('toRefreshFunction', () => {
+ it("should return 'undefined' when there are no faker calls", async () => {
+ // given
+ const method = newTestMethod({
+ examples: ['const a = 1;'],
+ });
+
+ // when
+ const result = await toRefreshFunction(method);
+
+ // then
+ expect(result).toBe('undefined');
+ });
+
+ it('should handle single line calls with semicolon', async () => {
+ // given
+ const method = newTestMethod({
+ examples: ['faker.number.int(); // 834135'],
+ });
+
+ // when
+ const result = await toRefreshFunction(method);
+
+ // then
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should handle single line calls without semicolon', async () => {
+ // given
+ const method = newTestMethod({
+ examples: ['faker.number.int() // 834135'],
+ });
+
+ // when
+ const result = await toRefreshFunction(method);
+
+ // then
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should handle multiple calls', async () => {
+ // given
+ const method = newTestMethod({
+ examples: ['faker.number.int()', 'faker.number.int()'],
+ });
+
+ // when
+ const result = await toRefreshFunction(method);
+
+ // then
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should handle multiline calls', async () => {
+ // given
+ const method = newTestMethod({
+ examples: 'faker.number.int({\n min: 1,\n max: 10\n})'.split('\n'),
+ });
+
+ // when
+ const result = await toRefreshFunction(method);
+
+ // then
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should handle properties after calls', async () => {
+ // given
+ const method = newTestMethod({
+ examples: ['faker.airline.airport().name'],
+ });
+
+ // when
+ const result = await toRefreshFunction(method);
+
+ // then
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
index c8ffb862..388a8f79 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,6 +14,8 @@
"exclude": [
"node_modules",
"dist",
+ // Ignore the generated API documentation
+ "docs/api",
// required for the signature related tests on macOS #2280
"test/scripts/apidocs/temp"
]