test: collections and new test ids and classes
This commit is contained in:
@@ -25,7 +25,9 @@ const ButtonContents = ({ children, icon, tooltip }) => {
|
|||||||
const BuiltInIcon = icons[icon];
|
const BuiltInIcon = icons[icon];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`${baseClass}__content`}>
|
<span
|
||||||
|
className={`${baseClass}__content`}
|
||||||
|
>
|
||||||
{tooltip && (
|
{tooltip && (
|
||||||
<Tooltip className={`${baseClass}__tooltip`}>
|
<Tooltip className={`${baseClass}__tooltip`}>
|
||||||
{tooltip}
|
{tooltip}
|
||||||
@@ -49,6 +51,7 @@ const ButtonContents = ({ children, icon, tooltip }) => {
|
|||||||
const Button: React.FC<Props> = (props) => {
|
const Button: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
|
id,
|
||||||
type = 'button',
|
type = 'button',
|
||||||
el,
|
el,
|
||||||
to,
|
to,
|
||||||
@@ -86,6 +89,7 @@ const Button: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buttonProps = {
|
const buttonProps = {
|
||||||
|
id,
|
||||||
type,
|
type,
|
||||||
className: classes,
|
className: classes,
|
||||||
disabled,
|
disabled,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { MouseEvent } from 'react';
|
|||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
className?: string,
|
className?: string,
|
||||||
|
id?: string,
|
||||||
type?: 'submit' | 'button',
|
type?: 'submit' | 'button',
|
||||||
el?: 'link' | 'anchor' | undefined,
|
el?: 'link' | 'anchor' | undefined,
|
||||||
to?: string,
|
to?: string,
|
||||||
@@ -12,6 +13,7 @@ export type Props = {
|
|||||||
icon?: React.ReactNode | ['chevron' | 'x' | 'plus' | 'edit'],
|
icon?: React.ReactNode | ['chevron' | 'x' | 'plus' | 'edit'],
|
||||||
iconStyle?: 'with-border' | 'without-border' | 'none',
|
iconStyle?: 'with-border' | 'without-border' | 'none',
|
||||||
buttonStyle?: 'primary' | 'secondary' | 'transparent' | 'error' | 'none' | 'icon-label',
|
buttonStyle?: 'primary' | 'secondary' | 'transparent' | 'error' | 'none' | 'icon-label',
|
||||||
|
buttonId?: string,
|
||||||
round?: boolean,
|
round?: boolean,
|
||||||
size?: 'small' | 'medium',
|
size?: 'small' | 'medium',
|
||||||
iconPosition?: 'left' | 'right',
|
iconPosition?: 'left' | 'right',
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
|||||||
const {
|
const {
|
||||||
title: titleFromProps,
|
title: titleFromProps,
|
||||||
id,
|
id,
|
||||||
|
buttonId,
|
||||||
collection: {
|
collection: {
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle,
|
useAsTitle,
|
||||||
@@ -78,6 +79,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
id={buttonId}
|
||||||
className={`${baseClass}__toggle`}
|
className={`${baseClass}__toggle`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -105,6 +107,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
|||||||
". Are you sure?
|
". Are you sure?
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
|
id="confirm-cancel"
|
||||||
buttonStyle="secondary"
|
buttonStyle="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={deleting ? undefined : () => toggle(modalSlug)}
|
onClick={deleting ? undefined : () => toggle(modalSlug)}
|
||||||
@@ -113,6 +116,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleting ? undefined : handleDelete}
|
onClick={deleting ? undefined : handleDelete}
|
||||||
|
id="confirm-delete"
|
||||||
>
|
>
|
||||||
{deleting ? 'Deleting...' : 'Confirm'}
|
{deleting ? 'Deleting...' : 'Confirm'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ import { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
|||||||
export type Props = {
|
export type Props = {
|
||||||
collection?: SanitizedCollectionConfig,
|
collection?: SanitizedCollectionConfig,
|
||||||
id?: string,
|
id?: string,
|
||||||
|
buttonId?: string,
|
||||||
title?: string,
|
title?: string,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const Duplicate: React.FC<Props> = ({ slug }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
id="action-duplicate"
|
||||||
buttonStyle="none"
|
buttonStyle="none"
|
||||||
className={baseClass}
|
className={baseClass}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ const Table: React.FC<Props> = ({ columns, data }) => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map((col, i) => (
|
{columns.map((col, i) => (
|
||||||
<th key={i}>
|
<th
|
||||||
|
key={i}
|
||||||
|
id={`heading-${col.accessor}`}
|
||||||
|
>
|
||||||
{col.components.Heading}
|
{col.components.Heading}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const Condition: React.FC<Props> = (props) => {
|
|||||||
<div className={`${baseClass}__actions`}>
|
<div className={`${baseClass}__actions`}>
|
||||||
<Button
|
<Button
|
||||||
icon="x"
|
icon="x"
|
||||||
|
className={`${baseClass}__actions-remove`}
|
||||||
round
|
round
|
||||||
buttonStyle="icon-label"
|
buttonStyle="icon-label"
|
||||||
iconStyle="with-border"
|
iconStyle="with-border"
|
||||||
@@ -116,6 +117,7 @@ const Condition: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon="plus"
|
icon="plus"
|
||||||
|
className={`${baseClass}__actions-add`}
|
||||||
round
|
round
|
||||||
buttonStyle="icon-label"
|
buttonStyle="icon-label"
|
||||||
iconStyle="with-border"
|
iconStyle="with-border"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import './index.scss';
|
|||||||
const baseClass = 'form-submit';
|
const baseClass = 'form-submit';
|
||||||
|
|
||||||
const FormSubmit: React.FC<Props> = (props) => {
|
const FormSubmit: React.FC<Props> = (props) => {
|
||||||
const { children, disabled: disabledFromProps, type = 'submit' } = props;
|
const { children, buttonId: id, disabled: disabledFromProps, type = 'submit' } = props;
|
||||||
const processing = useFormProcessing();
|
const processing = useFormProcessing();
|
||||||
const { disabled } = useForm();
|
const { disabled } = useForm();
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ const FormSubmit: React.FC<Props> = (props) => {
|
|||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
disabled={disabledFromProps || processing || disabled ? true : undefined}
|
disabled={disabledFromProps || processing || disabled ? true : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -134,7 +134,14 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
|||||||
<ul className={`${baseClass}__collection-actions`}>
|
<ul className={`${baseClass}__collection-actions`}>
|
||||||
{(permissions?.create?.permission) && (
|
{(permissions?.create?.permission) && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li>
|
<li>
|
||||||
|
<Link
|
||||||
|
id="action-create"
|
||||||
|
to={`${admin}/collections/${slug}/create`}
|
||||||
|
>
|
||||||
|
Create New
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
{!disableDuplicate && (
|
{!disableDuplicate && (
|
||||||
<li><DuplicateDocument slug={slug} /></li>
|
<li><DuplicateDocument slug={slug} /></li>
|
||||||
)}
|
)}
|
||||||
@@ -145,6 +152,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
|||||||
<DeleteDocument
|
<DeleteDocument
|
||||||
collection={collection}
|
collection={collection}
|
||||||
id={id}
|
id={id}
|
||||||
|
buttonId="action-delete"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
@@ -167,7 +175,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
{!collection.versions?.drafts && (
|
{!collection.versions?.drafts && (
|
||||||
<FormSubmit>Save</FormSubmit>
|
<FormSubmit buttonId="action-save">Save</FormSubmit>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||||
|
import { devUser } from '../../credentials';
|
||||||
import { buildConfig } from '../buildConfig';
|
import { buildConfig } from '../buildConfig';
|
||||||
|
|
||||||
export const slug = 'posts';
|
export const slug = 'posts';
|
||||||
@@ -26,6 +27,14 @@ export default buildConfig({
|
|||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await mapAsync([...Array(11)], async () => {
|
await mapAsync([...Array(11)], async () => {
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: slug,
|
collection: slug,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import type { Page } from '@playwright/test';
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import payload from '../../../src';
|
import payload from '../../../src';
|
||||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||||
import { initPayloadTest } from '../../helpers/configHelpers';
|
import { initPayloadE2E } from '../../helpers/configHelpers';
|
||||||
import { firstRegister, saveDocAndAssert } from '../helpers';
|
import { login, saveDocAndAssert } from '../helpers';
|
||||||
import type { Post } from './config';
|
import type { Post } from './config';
|
||||||
import { slug } from './config';
|
import { slug } from './config';
|
||||||
import { mapAsync } from '../../../src/utilities/mapAsync';
|
import { mapAsync } from '../../../src/utilities/mapAsync';
|
||||||
@@ -20,19 +20,14 @@ describe('collections', () => {
|
|||||||
let page: Page;
|
let page: Page;
|
||||||
|
|
||||||
beforeAll(async ({ browser }) => {
|
beforeAll(async ({ browser }) => {
|
||||||
const { serverURL } = await initPayloadTest({
|
const { serverURL } = await initPayloadE2E(__dirname);
|
||||||
__dirname,
|
|
||||||
init: {
|
|
||||||
local: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await clearDocs(); // Clear any seeded data from onInit
|
await clearDocs(); // Clear any seeded data from onInit
|
||||||
url = new AdminUrlUtil(serverURL, slug);
|
url = new AdminUrlUtil(serverURL, slug);
|
||||||
|
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
page = await context.newPage();
|
page = await context.newPage();
|
||||||
|
|
||||||
await firstRegister({ page, serverURL });
|
await login({ page, serverURL });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -50,13 +45,13 @@ describe('collections', () => {
|
|||||||
|
|
||||||
test('should navigate to collection - card', async () => {
|
test('should navigate to collection - card', async () => {
|
||||||
await page.goto(url.admin);
|
await page.goto(url.admin);
|
||||||
await page.locator('a:has-text("Posts")').click();
|
await page.locator(`#card-${slug}`).click();
|
||||||
expect(page.url()).toContain(url.list);
|
expect(page.url()).toContain(url.list);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('breadcrumbs - from card to dashboard', async () => {
|
test('breadcrumbs - from list to dashboard', async () => {
|
||||||
await page.goto(url.list);
|
await page.goto(url.list);
|
||||||
await page.locator('a:has-text("Dashboard")').click();
|
await page.locator('.step-nav a[href="/admin"]').click();
|
||||||
expect(page.url()).toContain(url.admin);
|
expect(page.url()).toContain(url.admin);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,7 +59,7 @@ describe('collections', () => {
|
|||||||
const { id } = await createPost();
|
const { id } = await createPost();
|
||||||
|
|
||||||
await page.goto(url.edit(id));
|
await page.goto(url.edit(id));
|
||||||
await page.locator('nav >> text=Posts').click();
|
await page.locator(`.step-nav >> text=${slug}`).click();
|
||||||
expect(page.url()).toContain(url.list);
|
expect(page.url()).toContain(url.list);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -73,14 +68,14 @@ describe('collections', () => {
|
|||||||
describe('CRUD', () => {
|
describe('CRUD', () => {
|
||||||
test('should create', async () => {
|
test('should create', async () => {
|
||||||
await page.goto(url.create);
|
await page.goto(url.create);
|
||||||
await page.locator('#title').fill(title);
|
await page.locator('#field-title').fill(title);
|
||||||
await page.locator('#description').fill(description);
|
await page.locator('#field-description').fill(description);
|
||||||
await page.click('text=Save', { delay: 100 });
|
await page.click('#action-save', { delay: 100 });
|
||||||
|
|
||||||
await saveDocAndAssert(page);
|
await saveDocAndAssert(page);
|
||||||
|
|
||||||
await expect(page.locator('#title')).toHaveValue(title);
|
await expect(page.locator('#field-title')).toHaveValue(title);
|
||||||
await expect(page.locator('#description')).toHaveValue(description);
|
await expect(page.locator('#field-description')).toHaveValue(description);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should read existing', async () => {
|
test('should read existing', async () => {
|
||||||
@@ -88,8 +83,8 @@ describe('collections', () => {
|
|||||||
|
|
||||||
await page.goto(url.edit(id));
|
await page.goto(url.edit(id));
|
||||||
|
|
||||||
await expect(page.locator('#title')).toHaveValue(title);
|
await expect(page.locator('#field-title')).toHaveValue(title);
|
||||||
await expect(page.locator('#description')).toHaveValue(description);
|
await expect(page.locator('#field-description')).toHaveValue(description);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update existing', async () => {
|
test('should update existing', async () => {
|
||||||
@@ -99,21 +94,21 @@ describe('collections', () => {
|
|||||||
|
|
||||||
const newTitle = 'new title';
|
const newTitle = 'new title';
|
||||||
const newDesc = 'new description';
|
const newDesc = 'new description';
|
||||||
await page.locator('#title').fill(newTitle);
|
await page.locator('#field-title').fill(newTitle);
|
||||||
await page.locator('#description').fill(newDesc);
|
await page.locator('#field-description').fill(newDesc);
|
||||||
|
|
||||||
await saveDocAndAssert(page);
|
await saveDocAndAssert(page);
|
||||||
|
|
||||||
await expect(page.locator('#title')).toHaveValue(newTitle);
|
await expect(page.locator('#field-title')).toHaveValue(newTitle);
|
||||||
await expect(page.locator('#description')).toHaveValue(newDesc);
|
await expect(page.locator('#field-description')).toHaveValue(newDesc);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should delete existing', async () => {
|
test('should delete existing', async () => {
|
||||||
const { id } = await createPost();
|
const { id } = await createPost();
|
||||||
|
|
||||||
await page.goto(url.edit(id));
|
await page.goto(url.edit(id));
|
||||||
await page.locator('button:has-text("Delete")').click();
|
await page.locator('#action-delete').click();
|
||||||
await page.locator('button:has-text("Confirm")').click();
|
await page.locator('#confirm-delete').click();
|
||||||
|
|
||||||
await expect(page.locator(`text=Post "${id}" successfully deleted.`)).toBeVisible();
|
await expect(page.locator(`text=Post "${id}" successfully deleted.`)).toBeVisible();
|
||||||
expect(page.url()).toContain(url.list);
|
expect(page.url()).toContain(url.list);
|
||||||
@@ -123,10 +118,10 @@ describe('collections', () => {
|
|||||||
const { id } = await createPost();
|
const { id } = await createPost();
|
||||||
|
|
||||||
await page.goto(url.edit(id));
|
await page.goto(url.edit(id));
|
||||||
await page.locator('button:has-text("Duplicate")').click();
|
await page.locator('#action-duplicate').click();
|
||||||
|
|
||||||
expect(page.url()).toContain(url.create);
|
expect(page.url()).toContain(url.create);
|
||||||
await page.locator('button:has-text("Save")').click();
|
await page.locator('#action-save').click();
|
||||||
expect(page.url()).not.toContain(id); // new id
|
expect(page.url()).not.toContain(id); // new id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -149,7 +144,7 @@ describe('collections', () => {
|
|||||||
test('toggle columns', async () => {
|
test('toggle columns', async () => {
|
||||||
const columnCountLocator = 'table >> thead >> tr >> th';
|
const columnCountLocator = 'table >> thead >> tr >> th';
|
||||||
await createPost();
|
await createPost();
|
||||||
await page.locator('button:has-text("Columns")').click();
|
await page.locator('.list-controls__toggle-columns').click();
|
||||||
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
||||||
|
|
||||||
const numberOfColumns = await page.locator(columnCountLocator).count();
|
const numberOfColumns = await page.locator(columnCountLocator).count();
|
||||||
@@ -170,13 +165,13 @@ describe('collections', () => {
|
|||||||
|
|
||||||
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
||||||
|
|
||||||
await page.locator('button:has-text("Filters")').click();
|
await page.locator('.list-controls__toggle-where').click();
|
||||||
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
await wait(1000); // Wait for column toggle UI, should probably use waitForSelector
|
||||||
|
|
||||||
await page.locator('text=Add filter').click();
|
await page.locator('.where-builder__add-first-filter').click();
|
||||||
|
|
||||||
const operatorField = page.locator('.condition >> .condition__operator');
|
const operatorField = page.locator('.condition__operator');
|
||||||
const valueField = page.locator('.condition >> .condition__value >> input');
|
const valueField = page.locator('.condition__value >> input');
|
||||||
|
|
||||||
await operatorField.click();
|
await operatorField.click();
|
||||||
|
|
||||||
@@ -192,7 +187,7 @@ describe('collections', () => {
|
|||||||
expect(firstId).toEqual(id);
|
expect(firstId).toEqual(id);
|
||||||
|
|
||||||
// Remove filter
|
// Remove filter
|
||||||
await page.locator('.condition >> .icon--x').click();
|
await page.locator('.condition__actions-remove').click();
|
||||||
await wait(1000);
|
await wait(1000);
|
||||||
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
||||||
});
|
});
|
||||||
@@ -237,14 +232,11 @@ describe('collections', () => {
|
|||||||
|
|
||||||
await expect(getTableItems()).toHaveCount(2);
|
await expect(getTableItems()).toHaveCount(2);
|
||||||
|
|
||||||
const chevrons = page.locator('table >> thead >> th >> button');
|
const upChevron = page.locator('#heading-id .sort-column__asc');
|
||||||
const upChevron = chevrons.first();
|
const downChevron = page.locator('#heading-id .sort-column__desc');
|
||||||
const downChevron = chevrons.nth(1);
|
|
||||||
|
|
||||||
const getFirstId = async () => getTableItems().first().locator('td').first()
|
const getFirstId = async () => page.locator('.row-1 .cell-id').innerText();
|
||||||
.innerText();
|
const getSecondId = async () => page.locator('.row-2 .cell-id').innerText();
|
||||||
const getSecondId = async () => getTableItems().nth(1).locator('td').first()
|
|
||||||
.innerText();
|
|
||||||
|
|
||||||
const firstId = await getFirstId();
|
const firstId = await getFirstId();
|
||||||
const secondId = await getSecondId();
|
const secondId = await getSecondId();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function login(args: LoginArgs): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveDocAndAssert(page: Page): Promise<void> {
|
export async function saveDocAndAssert(page: Page): Promise<void> {
|
||||||
await page.click('text=Save', { delay: 100 });
|
await page.click('#action-save', { delay: 100 });
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully');
|
await expect(page.locator('.Toastify')).toContainText('successfully');
|
||||||
expect(page.url()).not.toContain('create');
|
expect(page.url()).not.toContain('create');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import express from 'express';
|
|||||||
import type { CollectionConfig } from '../../src/collections/config/types';
|
import type { CollectionConfig } from '../../src/collections/config/types';
|
||||||
import type { InitOptions } from '../../src/config/types';
|
import type { InitOptions } from '../../src/config/types';
|
||||||
import payload from '../../src';
|
import payload from '../../src';
|
||||||
import { devUser } from '../credentials';
|
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
__dirname: string
|
__dirname: string
|
||||||
|
|||||||
Reference in New Issue
Block a user