mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 17:22:18 +03:00
test(core): add tests for page info ui (#5769)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/64419f79-46e8-4171-b853-5908841f827a.png)
This commit is contained in:
parent
bb8e601f82
commit
372b4da884
@ -0,0 +1,58 @@
|
|||||||
|
import { ConfirmModal } from '@affine/component';
|
||||||
|
import { WorkspacePropertiesAdapter } from '@affine/core/modules/workspace';
|
||||||
|
import type { PageInfoCustomPropertyMeta } from '@affine/core/modules/workspace/properties/schema';
|
||||||
|
import { Trans } from '@affine/i18n';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { PagePropertiesMetaManager } from './page-properties-manager';
|
||||||
|
|
||||||
|
export const ConfirmDeletePropertyModal = ({
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
property,
|
||||||
|
show,
|
||||||
|
}: {
|
||||||
|
property: PageInfoCustomPropertyMeta;
|
||||||
|
show: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const adapter = useService(WorkspacePropertiesAdapter);
|
||||||
|
const count = useMemo(() => {
|
||||||
|
const manager = new PagePropertiesMetaManager(adapter);
|
||||||
|
return manager.getPropertyRelatedPages(property.id)?.size || 0;
|
||||||
|
}, [adapter, property.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmModal
|
||||||
|
open={show}
|
||||||
|
closeButtonOptions={{
|
||||||
|
onClick: onCancel,
|
||||||
|
}}
|
||||||
|
title={t['com.affine.settings.workspace.properties.delete-property']()}
|
||||||
|
description={
|
||||||
|
<Trans
|
||||||
|
values={{
|
||||||
|
name: property.name,
|
||||||
|
count,
|
||||||
|
}}
|
||||||
|
i18nKey="com.affine.settings.workspace.properties.delete-property-prompt"
|
||||||
|
>
|
||||||
|
The <strong>{{ name: property.name } as any}</strong> property will be
|
||||||
|
removed from count doc(s). This action cannot be undone.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
cancelButtonOptions={{
|
||||||
|
onClick: onCancel,
|
||||||
|
}}
|
||||||
|
confirmButtonOptions={{
|
||||||
|
type: 'error',
|
||||||
|
children: t['Confirm'](),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -34,7 +34,7 @@ export const IconsSelectorPanel = ({
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
return (
|
return (
|
||||||
<Scrollable.Root>
|
<Scrollable.Root>
|
||||||
<div className={styles.menuHeader}>
|
<div role="heading" className={styles.menuHeader}>
|
||||||
{t['com.affine.page-properties.icons']()}
|
{t['com.affine.page-properties.icons']()}
|
||||||
</div>
|
</div>
|
||||||
<Scrollable.Viewport className={styles.iconsContainerScrollable}>
|
<Scrollable.Viewport className={styles.iconsContainerScrollable}>
|
||||||
|
@ -90,7 +90,9 @@ export const EditPropertyNameMenuItem = ({
|
|||||||
const iconName = getSafeIconName(property.icon, property.type);
|
const iconName = getSafeIconName(property.icon, property.type);
|
||||||
const onKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback(
|
const onKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback(
|
||||||
e => {
|
e => {
|
||||||
|
if (e.key !== 'Escape') {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
}
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onBlur(e.currentTarget.value);
|
onBlur(e.currentTarget.value);
|
||||||
|
@ -132,6 +132,10 @@ export class PagePropertiesMetaManager {
|
|||||||
}
|
}
|
||||||
return mapping;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPropertyRelatedPages(id: string) {
|
||||||
|
return this.getPropertyStatistics().get(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PagePropertiesManager {
|
export class PagePropertiesManager {
|
||||||
|
@ -212,7 +212,7 @@ export const propertyRowCell = style({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
lineHeight: '20px',
|
lineHeight: '22px',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
':focus-visible': {
|
':focus-visible': {
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
|
@ -60,6 +60,7 @@ import {
|
|||||||
|
|
||||||
import { AffinePageReference } from '../reference-link';
|
import { AffinePageReference } from '../reference-link';
|
||||||
import { managerContext, pageInfoCollapsedAtom } from './common';
|
import { managerContext, pageInfoCollapsedAtom } from './common';
|
||||||
|
import { ConfirmDeletePropertyModal } from './confirm-delete-property-modal';
|
||||||
import {
|
import {
|
||||||
getDefaultIconName,
|
getDefaultIconName,
|
||||||
nameToIcon,
|
nameToIcon,
|
||||||
@ -281,7 +282,11 @@ const VisibilityModeSelector = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div data-required={required} className={styles.selectorButton}>
|
<div
|
||||||
|
role="button"
|
||||||
|
data-required={required}
|
||||||
|
className={styles.selectorButton}
|
||||||
|
>
|
||||||
{required ? (
|
{required ? (
|
||||||
t['com.affine.page-properties.property.required']()
|
t['com.affine.page-properties.property.required']()
|
||||||
) : (
|
) : (
|
||||||
@ -305,7 +310,11 @@ export const PagePropertiesSettingsPopup = ({
|
|||||||
const menuItems = useMemo(() => {
|
const menuItems = useMemo(() => {
|
||||||
const options: MenuItemOption[] = [];
|
const options: MenuItemOption[] = [];
|
||||||
options.push(
|
options.push(
|
||||||
<div className={styles.menuHeader} style={{ minWidth: 320 }}>
|
<div
|
||||||
|
role="heading"
|
||||||
|
className={styles.menuHeader}
|
||||||
|
style={{ minWidth: 320 }}
|
||||||
|
>
|
||||||
{t['com.affine.page-properties.settings.title']()}
|
{t['com.affine.page-properties.settings.title']()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -327,7 +336,12 @@ export const PagePropertiesSettingsPopup = ({
|
|||||||
<MenuIcon>
|
<MenuIcon>
|
||||||
<Icon />
|
<Icon />
|
||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
<div className={styles.propertyRowName}>{name}</div>
|
<div
|
||||||
|
data-testid="page-property-setting-row-name"
|
||||||
|
className={styles.propertyRowName}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
<VisibilityModeSelector property={property} />
|
<VisibilityModeSelector property={property} />
|
||||||
</SortablePropertyRow>
|
</SortablePropertyRow>
|
||||||
);
|
);
|
||||||
@ -407,6 +421,8 @@ export const PagePropertyRowNameMenu = ({
|
|||||||
const [localProperty, setLocalProperty] = useState(() => ({ ...property }));
|
const [localProperty, setLocalProperty] = useState(() => ({ ...property }));
|
||||||
const nextVisibility = rotateVisibility(localProperty.visibility);
|
const nextVisibility = rotateVisibility(localProperty.visibility);
|
||||||
|
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
|
||||||
const handleFinishEditing = useCallback(() => {
|
const handleFinishEditing = useCallback(() => {
|
||||||
onFinishEditing();
|
onFinishEditing();
|
||||||
manager.updateCustomPropertyMeta(meta.id, localPropertyMeta);
|
manager.updateCustomPropertyMeta(meta.id, localPropertyMeta);
|
||||||
@ -445,14 +461,9 @@ export const PagePropertyRowNameMenu = ({
|
|||||||
},
|
},
|
||||||
[nextVisibility]
|
[nextVisibility]
|
||||||
);
|
);
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(() => {
|
||||||
(e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
manager.removeCustomProperty(property.id);
|
manager.removeCustomProperty(property.id);
|
||||||
},
|
}, [manager, property.id]);
|
||||||
[manager, property.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleIconChange = useCallback(
|
const handleIconChange = useCallback(
|
||||||
(icon: PagePropertyIcon) => {
|
(icon: PagePropertyIcon) => {
|
||||||
@ -496,12 +507,11 @@ export const PagePropertyRowNameMenu = ({
|
|||||||
type: 'danger',
|
type: 'danger',
|
||||||
icon: <DeleteIcon />,
|
icon: <DeleteIcon />,
|
||||||
text: t['com.affine.page-properties.property.remove-property'](),
|
text: t['com.affine.page-properties.property.remove-property'](),
|
||||||
onClick: handleDelete,
|
onClick: () => setShowDeleteModal(true),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return renderMenuItemOptions(options);
|
return renderMenuItemOptions(options);
|
||||||
}, [
|
}, [
|
||||||
handleDelete,
|
|
||||||
handleIconChange,
|
handleIconChange,
|
||||||
handleNameBlur,
|
handleNameBlur,
|
||||||
handleNameChange,
|
handleNameChange,
|
||||||
@ -513,6 +523,7 @@ export const PagePropertyRowNameMenu = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Menu
|
<Menu
|
||||||
rootOptions={{
|
rootOptions={{
|
||||||
open: editing,
|
open: editing,
|
||||||
@ -522,11 +533,26 @@ export const PagePropertyRowNameMenu = ({
|
|||||||
onClick(e) {
|
onClick(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
handleFinishEditing();
|
||||||
|
}
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
<ConfirmDeletePropertyModal
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
handleDelete();
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
show={showDeleteModal}
|
||||||
|
property={meta}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -607,7 +633,11 @@ export const PagePropertiesTableHeader = ({
|
|||||||
</div>
|
</div>
|
||||||
{properties.length === 0 || manager.readonly ? null : (
|
{properties.length === 0 || manager.readonly ? null : (
|
||||||
<PagePropertiesSettingsPopup>
|
<PagePropertiesSettingsPopup>
|
||||||
<IconButton type="plain" icon={<MoreHorizontalIcon />} />
|
<IconButton
|
||||||
|
data-testid="page-info-show-more"
|
||||||
|
type="plain"
|
||||||
|
icon={<MoreHorizontalIcon />}
|
||||||
|
/>
|
||||||
</PagePropertiesSettingsPopup>
|
</PagePropertiesSettingsPopup>
|
||||||
)}
|
)}
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
@ -802,7 +832,7 @@ export const PagePropertiesCreatePropertyMenuItems = ({
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const options: MenuItemOption[] = [];
|
const options: MenuItemOption[] = [];
|
||||||
options.push(
|
options.push(
|
||||||
<div className={styles.menuHeader}>
|
<div role="heading" className={styles.menuHeader}>
|
||||||
{t['com.affine.page-properties.create-property.menu.header']()}
|
{t['com.affine.page-properties.create-property.menu.header']()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -864,7 +894,7 @@ const PagePropertiesAddPropertyMenuItems = ({
|
|||||||
const menuItems = useMemo(() => {
|
const menuItems = useMemo(() => {
|
||||||
const options: MenuItemOption[] = [];
|
const options: MenuItemOption[] = [];
|
||||||
options.push(
|
options.push(
|
||||||
<div className={styles.menuHeader}>
|
<div role="heading" className={styles.menuHeader}>
|
||||||
{t['com.affine.page-properties.add-property.menu.header']()}
|
{t['com.affine.page-properties.add-property.menu.header']()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -47,7 +47,7 @@ const InlineTagsList = ({
|
|||||||
children,
|
children,
|
||||||
}: PropsWithChildren<InlineTagsListProps>) => {
|
}: PropsWithChildren<InlineTagsListProps>) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.inlineTagsContainer}>
|
<div className={styles.inlineTagsContainer} data-testid="inline-tags-list">
|
||||||
{value.map((tagId, idx) => {
|
{value.map((tagId, idx) => {
|
||||||
const tag = options.find(t => t.id === tagId);
|
const tag = options.find(t => t.id === tagId);
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
@ -251,7 +251,7 @@ export const TagsEditor = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tagsEditorRoot}>
|
<div data-testid="tags-editor-popup" className={styles.tagsEditorRoot}>
|
||||||
<div className={styles.tagsEditorSelectedTags}>
|
<div className={styles.tagsEditorSelectedTags}>
|
||||||
<InlineTagsList
|
<InlineTagsList
|
||||||
options={options}
|
options={options}
|
||||||
@ -282,6 +282,9 @@ export const TagsEditor = ({
|
|||||||
<div
|
<div
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
className={styles.tagSelectorItem}
|
className={styles.tagSelectorItem}
|
||||||
|
data-testid="tag-selector-item"
|
||||||
|
data-tag-id={tag.id}
|
||||||
|
data-tag-value={tag.value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onAddTag(tag.id);
|
onAddTag(tag.id);
|
||||||
}}
|
}}
|
||||||
@ -296,6 +299,7 @@ export const TagsEditor = ({
|
|||||||
})}
|
})}
|
||||||
{exactMatch || !inputValue ? null : (
|
{exactMatch || !inputValue ? null : (
|
||||||
<div
|
<div
|
||||||
|
data-testid="tag-selector-item"
|
||||||
className={styles.tagSelectorItem}
|
className={styles.tagSelectorItem}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
|
@ -269,6 +269,7 @@ const WorkspaceListItem = ({
|
|||||||
.map(({ key, title }) => {
|
.map(({ key, title }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-testid={`workspace-list-item-${key}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClick(key);
|
onClick(key);
|
||||||
}}
|
}}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button, ConfirmModal, IconButton, Menu } from '@affine/component';
|
import { Button, IconButton, Menu } from '@affine/component';
|
||||||
import { SettingHeader } from '@affine/component/setting-components';
|
import { SettingHeader } from '@affine/component/setting-components';
|
||||||
import { useWorkspacePropertiesAdapter } from '@affine/core/hooks/use-affine-adapter';
|
import { useWorkspacePropertiesAdapter } from '@affine/core/hooks/use-affine-adapter';
|
||||||
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
||||||
@ -28,6 +28,7 @@ import {
|
|||||||
PagePropertiesMetaManager,
|
PagePropertiesMetaManager,
|
||||||
type PagePropertyIcon,
|
type PagePropertyIcon,
|
||||||
} from '../../../page-properties';
|
} from '../../../page-properties';
|
||||||
|
import { ConfirmDeletePropertyModal } from '../../../page-properties/confirm-delete-property-modal';
|
||||||
import {
|
import {
|
||||||
EditPropertyNameMenuItem,
|
EditPropertyNameMenuItem,
|
||||||
type MenuItemOption,
|
type MenuItemOption,
|
||||||
@ -54,57 +55,9 @@ const Divider = () => {
|
|||||||
return <div className={styles.divider} />;
|
return <div className={styles.divider} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConfirmDeletePropertyModal = ({
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
property,
|
|
||||||
count,
|
|
||||||
show,
|
|
||||||
}: {
|
|
||||||
property: PageInfoCustomPropertyMeta;
|
|
||||||
count: number;
|
|
||||||
show: boolean;
|
|
||||||
onConfirm: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
}) => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfirmModal
|
|
||||||
open={show}
|
|
||||||
closeButtonOptions={{
|
|
||||||
onClick: onCancel,
|
|
||||||
}}
|
|
||||||
title={t['com.affine.settings.workspace.properties.delete-property']()}
|
|
||||||
description={
|
|
||||||
<Trans
|
|
||||||
values={{
|
|
||||||
name: property.name,
|
|
||||||
count,
|
|
||||||
}}
|
|
||||||
i18nKey="com.affine.settings.workspace.properties.delete-property-prompt"
|
|
||||||
>
|
|
||||||
The <strong>{{ name: property.name } as any}</strong> property will be
|
|
||||||
removed from count doc(s). This action cannot be undone.
|
|
||||||
</Trans>
|
|
||||||
}
|
|
||||||
onConfirm={onConfirm}
|
|
||||||
cancelButtonOptions={{
|
|
||||||
onClick: onCancel,
|
|
||||||
}}
|
|
||||||
confirmButtonOptions={{
|
|
||||||
type: 'error',
|
|
||||||
children: t['Confirm'](),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditPropertyButton = ({
|
const EditPropertyButton = ({
|
||||||
property,
|
property,
|
||||||
count,
|
|
||||||
}: {
|
}: {
|
||||||
count: number;
|
|
||||||
property: PageInfoCustomPropertyMeta;
|
property: PageInfoCustomPropertyMeta;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
@ -234,7 +187,6 @@ const EditPropertyButton = ({
|
|||||||
onCancel={() => setShowDeleteModal(false)}
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
show={showDeleteModal}
|
show={showDeleteModal}
|
||||||
property={property}
|
property={property}
|
||||||
count={count}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -281,7 +233,7 @@ const CustomPropertyRow = ({
|
|||||||
{t['com.affine.page-properties.property.required']()}
|
{t['com.affine.page-properties.property.required']()}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<EditPropertyButton property={property} count={relatedPages.length} />
|
<EditPropertyButton property={property} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,8 @@ export const TagItem = ({
|
|||||||
data-testid="page-tag"
|
data-testid="page-tag"
|
||||||
className={styles.tag}
|
className={styles.tag}
|
||||||
data-idx={idx}
|
data-idx={idx}
|
||||||
|
data-tag-id={tag.id}
|
||||||
|
data-tag-value={tag.value}
|
||||||
title={tag.value}
|
title={tag.value}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
@ -59,7 +61,11 @@ export const TagItem = ({
|
|||||||
/>
|
/>
|
||||||
<div className={styles.tagLabel}>{tag.value}</div>
|
<div className={styles.tagLabel}>{tag.value}</div>
|
||||||
{onRemoved ? (
|
{onRemoved ? (
|
||||||
<div className={styles.tagRemove} onClick={handleRemove}>
|
<div
|
||||||
|
data-testid="remove-tag-button"
|
||||||
|
className={styles.tagRemove}
|
||||||
|
onClick={handleRemove}
|
||||||
|
>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -1107,7 +1107,7 @@
|
|||||||
"com.affine.page-properties.backlinks": "Backlinks",
|
"com.affine.page-properties.backlinks": "Backlinks",
|
||||||
"com.affine.page-properties.page-info": "Page Info",
|
"com.affine.page-properties.page-info": "Page Info",
|
||||||
"com.affine.page-properties.settings.title": "customize properties",
|
"com.affine.page-properties.settings.title": "customize properties",
|
||||||
"com.affine.page-properties.add-property": "Add a property",
|
"com.affine.page-properties.add-property": "Add property",
|
||||||
"com.affine.page-properties.add-property.menu.header": "Properties",
|
"com.affine.page-properties.add-property.menu.header": "Properties",
|
||||||
"com.affine.page-properties.create-property.menu.header": "Type",
|
"com.affine.page-properties.create-property.menu.header": "Type",
|
||||||
"com.affine.page-properties.add-property.menu.create": "Create property",
|
"com.affine.page-properties.add-property.menu.create": "Create property",
|
||||||
|
@ -106,7 +106,7 @@ test('use monthpicker to modify the month of datepicker', async ({ page }) => {
|
|||||||
await checkDatePickerMonth(page, nextMonth);
|
await checkDatePickerMonth(page, nextMonth);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('allow creation of filters by tags', async ({ page }) => {
|
test('allow creation of filters by tags', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
|
@ -137,27 +137,6 @@ test('edit collection and change filter date', async ({ page }) => {
|
|||||||
expect(await first.textContent()).toBe('123');
|
expect(await first.textContent()).toBe('123');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('create temporary filter by click tag', async ({ page }) => {
|
|
||||||
await clickNewPageButton(page);
|
|
||||||
await getBlockSuiteEditorTitle(page).click();
|
|
||||||
await getBlockSuiteEditorTitle(page).fill('test page');
|
|
||||||
await page.locator('page-meta-tags').click();
|
|
||||||
await page.locator('.add-tag').click();
|
|
||||||
await page.keyboard.type('TODO Tag');
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.locator('.tag', { hasText: 'TODO Tag' }).click();
|
|
||||||
const cell = page.getByTestId('page-list-item-title').getByText('test page');
|
|
||||||
await expect(cell).toBeVisible();
|
|
||||||
expect(await page.getByTestId('page-list-item').count()).toBe(1);
|
|
||||||
await page.getByTestId('filter-arg').click();
|
|
||||||
|
|
||||||
await page.getByTestId('multi-select-TODO Tag').click();
|
|
||||||
expect(
|
|
||||||
await page.getByTestId('page-list-item').count()
|
|
||||||
).toBeGreaterThanOrEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('add collection from sidebar', async ({ page }) => {
|
test('add collection from sidebar', async ({ page }) => {
|
||||||
await removeOnboardingPages(page);
|
await removeOnboardingPages(page);
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
|
@ -48,6 +48,7 @@ test('page delete -> create new page -> refresh page -> new page should be appea
|
|||||||
page,
|
page,
|
||||||
workspace,
|
workspace,
|
||||||
}) => {
|
}) => {
|
||||||
|
test.slow();
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { test } from '@affine-test/kit/playwright';
|
import { test } from '@affine-test/kit/playwright';
|
||||||
|
import { clickPageModeButton } from '@affine-test/kit/utils/editor';
|
||||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
@ -135,8 +136,9 @@ test("Deleted page's reference will not be shown in sidebar", async ({
|
|||||||
test('Add new favorite page via sidebar', async ({ page }) => {
|
test('Add new favorite page via sidebar', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickNewPageButton(page);
|
|
||||||
await page.getByTestId('slider-bar-add-favorite-button').first().click();
|
await page.getByTestId('slider-bar-add-favorite-button').first().click();
|
||||||
|
await clickPageModeButton(page);
|
||||||
await waitForEmptyEditor(page);
|
await waitForEmptyEditor(page);
|
||||||
|
|
||||||
// enter random page title
|
// enter random page title
|
||||||
|
243
tests/affine-local/e2e/page-properties.spec.ts
Normal file
243
tests/affine-local/e2e/page-properties.spec.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
/* eslint-disable unicorn/prefer-dom-node-dataset */
|
||||||
|
import { test } from '@affine-test/kit/playwright';
|
||||||
|
import { clickPageModeButton } from '@affine-test/kit/utils/editor';
|
||||||
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
|
import { dragTo, waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
|
||||||
|
import {
|
||||||
|
addCustomProperty,
|
||||||
|
changePropertyVisibility,
|
||||||
|
clickPropertyValue,
|
||||||
|
closeTagsEditor,
|
||||||
|
expectTagsVisible,
|
||||||
|
getPropertyValueLocator,
|
||||||
|
openTagsEditor,
|
||||||
|
openWorkspaceProperties,
|
||||||
|
removeSelectedTag,
|
||||||
|
searchAndCreateTag,
|
||||||
|
} from '@affine-test/kit/utils/properties';
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await clickPageModeButton(page);
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allow create tag', async ({ page }) => {
|
||||||
|
await openTagsEditor(page);
|
||||||
|
await searchAndCreateTag(page, 'Test1');
|
||||||
|
await searchAndCreateTag(page, 'Test2');
|
||||||
|
await closeTagsEditor(page);
|
||||||
|
await expectTagsVisible(page, ['Test1', 'Test2']);
|
||||||
|
|
||||||
|
await openTagsEditor(page);
|
||||||
|
await removeSelectedTag(page, 'Test1');
|
||||||
|
await closeTagsEditor(page);
|
||||||
|
await expectTagsVisible(page, ['Test2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add custom property', async ({ page }) => {
|
||||||
|
await addCustomProperty(page, 'Text');
|
||||||
|
await addCustomProperty(page, 'Number');
|
||||||
|
await addCustomProperty(page, 'Date');
|
||||||
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add custom property & edit', async ({ page }) => {
|
||||||
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
await expect(
|
||||||
|
getPropertyValueLocator(page, 'Checkbox').locator('input')
|
||||||
|
).not.toBeChecked();
|
||||||
|
await clickPropertyValue(page, 'Checkbox');
|
||||||
|
await expect(
|
||||||
|
getPropertyValueLocator(page, 'Checkbox').locator('input')
|
||||||
|
).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('property table reordering', async ({ page }) => {
|
||||||
|
await addCustomProperty(page, 'Text');
|
||||||
|
await addCustomProperty(page, 'Number');
|
||||||
|
await addCustomProperty(page, 'Date');
|
||||||
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
|
||||||
|
await dragTo(
|
||||||
|
page,
|
||||||
|
page.locator('[data-testid="page-property-row-name"]:has-text("Text")'),
|
||||||
|
page.locator(
|
||||||
|
'[data-testid="page-property-row-name"]:has-text("Checkbox") + div'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// new order should be (Tags), Number, Date, Checkbox, Text
|
||||||
|
for (const [index, property] of [
|
||||||
|
'Tags',
|
||||||
|
'Number',
|
||||||
|
'Date',
|
||||||
|
'Checkbox',
|
||||||
|
'Text',
|
||||||
|
].entries()) {
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId('page-property-row')
|
||||||
|
.nth(index)
|
||||||
|
.getByTestId('page-property-row-name')
|
||||||
|
).toHaveText(property);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('page info show more will not should by default when there is no properties', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// by default page info show more should not show
|
||||||
|
await expect(page.getByTestId('page-info-show-more')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('page info show more will show all properties', async ({ page }) => {
|
||||||
|
await addCustomProperty(page, 'Text');
|
||||||
|
await addCustomProperty(page, 'Number');
|
||||||
|
await addCustomProperty(page, 'Date');
|
||||||
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
|
||||||
|
await expect(page.getByTestId('page-info-show-more')).toBeVisible();
|
||||||
|
await page.click('[data-testid="page-info-show-more"]');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', {
|
||||||
|
name: 'customize properties',
|
||||||
|
})
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// new order should be (Tags), Number, Date, Checkbox, Text
|
||||||
|
for (const [index, property] of [
|
||||||
|
'Text',
|
||||||
|
'Number',
|
||||||
|
'Date',
|
||||||
|
'Checkbox',
|
||||||
|
].entries()) {
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId('page-properties-settings-menu-item')
|
||||||
|
.nth(index)
|
||||||
|
.getByTestId('page-property-setting-row-name')
|
||||||
|
).toHaveText(property);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('change page properties visibility', async ({ page }) => {
|
||||||
|
await addCustomProperty(page, 'Text');
|
||||||
|
await addCustomProperty(page, 'Number');
|
||||||
|
await addCustomProperty(page, 'Date');
|
||||||
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
|
||||||
|
// add some number to number property
|
||||||
|
await clickPropertyValue(page, 'Number');
|
||||||
|
await page.locator('input[type=number]').fill('123');
|
||||||
|
|
||||||
|
await changePropertyVisibility(page, 'Text', 'Hide in view');
|
||||||
|
await changePropertyVisibility(page, 'Number', 'Hide in view when empty');
|
||||||
|
|
||||||
|
// text property should not be visible
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="page-property-row-name"]:has-text("Text")')
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
// number property should be visible
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="page-property-row-name"]:has-text("Number")')
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('check if added property is also in workspace settings', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await addCustomProperty(page, 'Text');
|
||||||
|
await openWorkspaceProperties(page);
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid=custom-property-row]:has-text("Text")')
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edit property name', async ({ page }) => {
|
||||||
|
await addCustomProperty(page, 'Text');
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="page-property-row-name"]:has-text("Text")')
|
||||||
|
.click();
|
||||||
|
await expect(page.locator('[data-radix-menu-content]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-radix-menu-content] input')).toHaveValue(
|
||||||
|
'Text'
|
||||||
|
);
|
||||||
|
await page.locator('[data-radix-menu-content] input').fill('New Text');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await expect(page.locator('[data-radix-menu-content] input')).toHaveValue(
|
||||||
|
'New Text'
|
||||||
|
);
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// check if the property name is also updated in workspace settings
|
||||||
|
await openWorkspaceProperties(page);
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid=custom-property-row]:has-text("New Text")')
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete property via property popup', async ({ page }) => {
|
||||||
|
await addCustomProperty(page, 'Text');
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="page-property-row-name"]:has-text("Text")')
|
||||||
|
.click();
|
||||||
|
await expect(page.locator('[data-radix-menu-content]')).toBeVisible();
|
||||||
|
await page
|
||||||
|
.locator('[data-radix-menu-content]')
|
||||||
|
.getByRole('menuitem', {
|
||||||
|
name: 'Remove property',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
// confirm delete dialog should show
|
||||||
|
await expect(page.getByRole('dialog')).toContainText(
|
||||||
|
`The "Text" property will be remove from 1 doc(s). This action cannot be undone.`
|
||||||
|
);
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Confirm',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
// check if the property is removed
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="page-property-row-name"]:has-text("Text")')
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create a required property', async ({ page }) => {
|
||||||
|
await openWorkspaceProperties(page);
|
||||||
|
await addCustomProperty(page, 'Text', true);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="custom-property-row"]:has-text("Text")')
|
||||||
|
.getByRole('button')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', {
|
||||||
|
name: 'Set as required property',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="custom-property-row"]:has-text("Text")')
|
||||||
|
).toContainText('Required');
|
||||||
|
|
||||||
|
// close workspace settings
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// check if the property is also required in page properties
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="page-property-row-name"]:has-text("Text")')
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// check if the required property is also listed in the show more menu
|
||||||
|
await page.click('[data-testid="page-info-show-more"]');
|
||||||
|
await expect(
|
||||||
|
page.locator(
|
||||||
|
'[data-testid="page-properties-settings-menu-item"]:has-text("Text")'
|
||||||
|
)
|
||||||
|
).toContainText('Required');
|
||||||
|
});
|
@ -126,8 +126,9 @@ export const createPageWithTag = async (
|
|||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await getBlockSuiteEditorTitle(page).click();
|
await getBlockSuiteEditorTitle(page).click();
|
||||||
await getBlockSuiteEditorTitle(page).fill('test page');
|
await getBlockSuiteEditorTitle(page).fill('test page');
|
||||||
await page.locator('page-meta-tags').click();
|
await page
|
||||||
await page.locator('.add-tag').click();
|
.locator('[data-testid="page-property-row"][data-property="tags"]')
|
||||||
|
.click();
|
||||||
for (const name of options.tags) {
|
for (const name of options.tags) {
|
||||||
await createTag(page, name);
|
await createTag(page, name);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,6 @@ export async function clickNewPageButton(page: Page) {
|
|||||||
await page.getByTestId('sidebar-new-page-button').click({
|
await page.getByTestId('sidebar-new-page-button').click({
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
await expect(page.locator('.doc-title-container-empty')).toBeVisible();
|
|
||||||
await waitForEmptyEditor(page);
|
await waitForEmptyEditor(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
178
tests/kit/utils/properties.ts
Normal file
178
tests/kit/utils/properties.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export const getPropertyValueLocator = (page: Page, property: string) => {
|
||||||
|
return page.locator(
|
||||||
|
`[data-testid="page-property-row-name"]:has-text("${property}") + *`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clickPropertyValue = async (page: Page, property: string) => {
|
||||||
|
await getPropertyValueLocator(page, property).click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openTagsEditor = async (page: Page) => {
|
||||||
|
await clickPropertyValue(page, 'tags');
|
||||||
|
await expect(page.getByTestId('tags-editor-popup')).toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const closeTagsEditor = async (page: Page) => {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await expect(page.getByTestId('tags-editor-popup')).not.toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clickTagFromSelector = async (page: Page, name: string) => {
|
||||||
|
// assume that the tags editor is already open
|
||||||
|
await page
|
||||||
|
.locator(`[data-testid="tag-selector-item"][data-tag-value="${name}"]`)
|
||||||
|
.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeSelectedTag = async (page: Page, name: string) => {
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
`[data-testid="tags-editor-popup"] [data-testid="inline-tags-list"] [data-tag-value="${name}"] [data-testid="remove-tag-button"]`
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterTags = async (page: Page, filter: string) => {
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
'[data-testid="tags-editor-popup"] [data-testid="inline-tags-list"] input'
|
||||||
|
)
|
||||||
|
.fill(filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchAndCreateTag = async (page: Page, name: string) => {
|
||||||
|
await filterTags(page, name);
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
'[data-testid="tags-editor-popup"] [data-testid="tag-selector-item"]:has-text("Create ")'
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expectTagsVisible = async (page: Page, tags: string[]) => {
|
||||||
|
const tagListPanel = page
|
||||||
|
.getByTestId('page-property-row')
|
||||||
|
.getByTestId('inline-tags-list');
|
||||||
|
|
||||||
|
expect(await tagListPanel.locator('[data-tag-value]').count()).toBe(
|
||||||
|
tags.length
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
await expect(
|
||||||
|
tagListPanel.locator(`[data-tag-value="${tag}"]`)
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clickAddPropertyButton = async (page: Page) => {
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Add property',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addCustomProperty = async (
|
||||||
|
page: Page,
|
||||||
|
type: string,
|
||||||
|
inSettings?: boolean
|
||||||
|
) => {
|
||||||
|
await clickAddPropertyButton(page);
|
||||||
|
if (!inSettings) {
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', {
|
||||||
|
name: 'Properties',
|
||||||
|
})
|
||||||
|
).toBeVisible();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', {
|
||||||
|
name: 'Create property',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', {
|
||||||
|
name: 'Type',
|
||||||
|
})
|
||||||
|
).toBeVisible();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', {
|
||||||
|
name: type,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
if (!inSettings) {
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByRole('menuitem', {
|
||||||
|
name: type,
|
||||||
|
})
|
||||||
|
.locator('.selected')
|
||||||
|
).toBeVisible();
|
||||||
|
await page.keyboard.press('Escape', {
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expectPropertyOrdering = async (
|
||||||
|
page: Page,
|
||||||
|
properties: string[]
|
||||||
|
) => {
|
||||||
|
for (let i = 0; i < properties.length; i++) {
|
||||||
|
await expect(
|
||||||
|
page.locator(`[data-testid="page-property-row-name"])`).nth(i)
|
||||||
|
).toHaveText(properties[i]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openWorkspaceProperties = async (page: Page) => {
|
||||||
|
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="workspace-list-item"] .setting-name')
|
||||||
|
.click();
|
||||||
|
await page.getByTestId('workspace-list-item-properties').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectVisibilitySelector = async (
|
||||||
|
page: Page,
|
||||||
|
name: string,
|
||||||
|
option: string
|
||||||
|
) => {
|
||||||
|
await page
|
||||||
|
.getByRole('menu')
|
||||||
|
.locator(
|
||||||
|
`[data-testid="page-properties-settings-menu-item"]:has-text("${name}")`
|
||||||
|
)
|
||||||
|
.getByRole('button')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('menu')
|
||||||
|
.last()
|
||||||
|
.getByRole('menuitem', {
|
||||||
|
name: option,
|
||||||
|
exact: true,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changePropertyVisibility = async (
|
||||||
|
page: Page,
|
||||||
|
name: string,
|
||||||
|
option: string
|
||||||
|
) => {
|
||||||
|
await expect(page.getByTestId('page-info-show-more')).toBeVisible();
|
||||||
|
await page.click('[data-testid="page-info-show-more"]');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', {
|
||||||
|
name: 'customize properties',
|
||||||
|
})
|
||||||
|
).toBeVisible();
|
||||||
|
await selectVisibilitySelector(page, name, option);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user