mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
671 lines
19 KiB
TypeScript
671 lines
19 KiB
TypeScript
import { expect } from '@playwright/test';
|
|
|
|
import {
|
|
dragBetweenCoords,
|
|
enterPlaygroundRoom,
|
|
focusDatabaseTitle,
|
|
getBoundingBox,
|
|
initDatabaseDynamicRowWithData,
|
|
initDatabaseRowWithData,
|
|
initEmptyDatabaseState,
|
|
pressArrowLeft,
|
|
pressArrowRight,
|
|
pressBackspace,
|
|
pressEnter,
|
|
pressEscape,
|
|
pressShiftEnter,
|
|
redoByKeyboard,
|
|
selectAllByKeyboard,
|
|
setInlineRangeInInlineEditor,
|
|
switchReadonly,
|
|
type,
|
|
undoByClick,
|
|
undoByKeyboard,
|
|
waitNextFrame,
|
|
} from '../utils/actions/index.js';
|
|
import {
|
|
assertBlockProps,
|
|
assertInlineEditorDeltas,
|
|
assertRowCount,
|
|
} from '../utils/asserts.js';
|
|
import { test } from '../utils/playwright.js';
|
|
import { getFormatBar } from '../utils/query.js';
|
|
import {
|
|
assertColumnWidth,
|
|
assertDatabaseCellRichTexts,
|
|
assertDatabaseSearching,
|
|
assertDatabaseTitleText,
|
|
blurDatabaseSearch,
|
|
clickColumnType,
|
|
clickDatabaseOutside,
|
|
focusDatabaseHeader,
|
|
focusDatabaseSearch,
|
|
getDatabaseBodyCell,
|
|
getDatabaseHeaderColumn,
|
|
getFirstColumnCell,
|
|
initDatabaseColumn,
|
|
switchColumnType,
|
|
} from './actions.js';
|
|
|
|
test('edit database block title and create new rows', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
const locator = page.locator('affine-database');
|
|
await expect(locator).toBeVisible();
|
|
const dbTitle = 'Database 1';
|
|
await assertBlockProps(page, '2', {
|
|
title: dbTitle,
|
|
});
|
|
await focusDatabaseTitle(page);
|
|
await selectAllByKeyboard(page);
|
|
await pressBackspace(page);
|
|
|
|
const expected = 'hello';
|
|
await type(page, expected);
|
|
await assertBlockProps(page, '2', {
|
|
title: 'hello',
|
|
});
|
|
await undoByClick(page);
|
|
await assertBlockProps(page, '2', {
|
|
title: 'Database 1',
|
|
});
|
|
await initDatabaseRowWithData(page, '');
|
|
await initDatabaseRowWithData(page, '');
|
|
await assertRowCount(page, 2);
|
|
await waitNextFrame(page, 100);
|
|
await pressEscape(page);
|
|
await undoByClick(page);
|
|
await undoByClick(page);
|
|
await assertRowCount(page, 0);
|
|
});
|
|
|
|
test('edit column title', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page, '1');
|
|
|
|
// first added column
|
|
const { column } = await getDatabaseHeaderColumn(page, 1);
|
|
expect(await column.innerText()).toBe('1');
|
|
|
|
await undoByClick(page);
|
|
expect(await column.innerText()).toBe('Column 1');
|
|
});
|
|
|
|
test('should modify the value when the input loses focus', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await switchColumnType(page, 'Number');
|
|
await initDatabaseDynamicRowWithData(page, '1', true);
|
|
|
|
await clickDatabaseOutside(page);
|
|
const cell = getFirstColumnCell(page, 'number');
|
|
const text = await cell.textContent();
|
|
expect(text?.trim()).toBe('1');
|
|
});
|
|
|
|
test('should rich-text column support soft enter', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await switchColumnType(page, 'Text');
|
|
await initDatabaseDynamicRowWithData(page, '123', true);
|
|
|
|
const cell = getFirstColumnCell(page, 'affine-database-rich-text');
|
|
await cell.click();
|
|
await pressArrowLeft(page);
|
|
await pressEnter(page);
|
|
await assertDatabaseCellRichTexts(page, { text: '123' });
|
|
|
|
await cell.click();
|
|
await pressArrowRight(page);
|
|
await pressArrowLeft(page);
|
|
await pressShiftEnter(page);
|
|
await pressEnter(page);
|
|
await assertDatabaseCellRichTexts(page, { text: '12\n3' });
|
|
});
|
|
|
|
test('should the multi-select mode work correctly', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await initDatabaseDynamicRowWithData(page, '1', true);
|
|
await pressEscape(page);
|
|
await initDatabaseDynamicRowWithData(page, '2');
|
|
await pressEscape(page);
|
|
const cell = getFirstColumnCell(page, 'select-selected');
|
|
expect(await cell.count()).toBe(2);
|
|
expect(await cell.nth(0).innerText()).toBe('1');
|
|
expect(await cell.nth(1).innerText()).toBe('2');
|
|
});
|
|
|
|
test('should database search work', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await initDatabaseRowWithData(page, 'text1');
|
|
await initDatabaseDynamicRowWithData(page, '123', false);
|
|
await pressEscape(page);
|
|
await initDatabaseRowWithData(page, 'text2');
|
|
await initDatabaseDynamicRowWithData(page, 'a', false);
|
|
await pressEscape(page);
|
|
await initDatabaseRowWithData(page, 'text3');
|
|
await initDatabaseDynamicRowWithData(page, '26', false);
|
|
await pressEscape(page);
|
|
// search for '2'
|
|
await focusDatabaseSearch(page);
|
|
await type(page, '2');
|
|
const rows = page.locator('.affine-database-block-row');
|
|
expect(await rows.count()).toBe(3);
|
|
|
|
// search for '23'
|
|
await type(page, '3');
|
|
expect(await rows.count()).toBe(1);
|
|
|
|
const cell = page.locator('.select-selected');
|
|
expect(await cell.innerText()).toBe('123');
|
|
|
|
// clear search input
|
|
const closeIcon = page.locator('.close-icon');
|
|
await closeIcon.click();
|
|
expect(await rows.count()).toBe(3);
|
|
});
|
|
|
|
test('should database search input displayed correctly', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await focusDatabaseSearch(page);
|
|
await blurDatabaseSearch(page);
|
|
await assertDatabaseSearching(page, false);
|
|
|
|
await focusDatabaseSearch(page);
|
|
await type(page, '2');
|
|
await blurDatabaseSearch(page);
|
|
await assertDatabaseSearching(page, true);
|
|
|
|
await focusDatabaseSearch(page);
|
|
await pressBackspace(page);
|
|
await blurDatabaseSearch(page);
|
|
await assertDatabaseSearching(page, false);
|
|
|
|
await focusDatabaseSearch(page);
|
|
await type(page, '2');
|
|
const closeIcon = page.locator('.close-icon');
|
|
await closeIcon.click();
|
|
await blurDatabaseSearch(page);
|
|
await assertDatabaseSearching(page, false);
|
|
|
|
await focusDatabaseSearch(page);
|
|
await type(page, '2');
|
|
await pressEscape(page);
|
|
await blurDatabaseSearch(page);
|
|
await assertDatabaseSearching(page, false);
|
|
});
|
|
|
|
test('should database title and rich-text support undo/redo', async ({
|
|
page,
|
|
}) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await switchColumnType(page, 'Text');
|
|
await initDatabaseDynamicRowWithData(page, '123', true);
|
|
await undoByKeyboard(page);
|
|
await assertDatabaseCellRichTexts(page, { text: '' });
|
|
await pressEscape(page);
|
|
await redoByKeyboard(page);
|
|
await assertDatabaseCellRichTexts(page, { text: '123' });
|
|
|
|
await focusDatabaseTitle(page);
|
|
await type(page, 'abc');
|
|
await assertDatabaseTitleText(page, 'Database 1abc');
|
|
await undoByKeyboard(page);
|
|
await assertDatabaseTitleText(page, 'Database 1');
|
|
await redoByKeyboard(page);
|
|
await assertDatabaseTitleText(page, 'Database 1abc');
|
|
});
|
|
|
|
test('should support drag to change column width', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
const headerColumns = page.locator('.affine-database-column');
|
|
const titleColumn = headerColumns.nth(0);
|
|
const normalColumn = headerColumns.nth(1);
|
|
|
|
const dragDistance = 100;
|
|
const titleColumnWidth = 260;
|
|
const normalColumnWidth = 180;
|
|
|
|
await assertColumnWidth(titleColumn, titleColumnWidth - 1);
|
|
const box = await assertColumnWidth(normalColumn, normalColumnWidth - 1);
|
|
|
|
await dragBetweenCoords(
|
|
page,
|
|
{ x: box.x, y: box.y },
|
|
{ x: box.x + dragDistance, y: box.y },
|
|
{
|
|
steps: 50,
|
|
beforeMouseUp: async () => {
|
|
await waitNextFrame(page);
|
|
},
|
|
}
|
|
);
|
|
|
|
await assertColumnWidth(titleColumn, titleColumnWidth + dragDistance);
|
|
await assertColumnWidth(normalColumn, normalColumnWidth - 1);
|
|
|
|
await undoByClick(page);
|
|
await assertColumnWidth(titleColumn, titleColumnWidth - 1);
|
|
await assertColumnWidth(normalColumn, normalColumnWidth - 1);
|
|
});
|
|
|
|
test('should display the add column button on the right side of database correctly', async ({
|
|
page,
|
|
}) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
const normalColumn = page.locator('.affine-database-column').nth(1);
|
|
|
|
const addColumnBtn = page.locator('.header-add-column-button');
|
|
|
|
const box = await getBoundingBox(normalColumn);
|
|
await dragBetweenCoords(
|
|
page,
|
|
{ x: box.x, y: box.y },
|
|
{ x: box.x + 400, y: box.y },
|
|
{
|
|
steps: 50,
|
|
beforeMouseUp: async () => {
|
|
await waitNextFrame(page);
|
|
},
|
|
}
|
|
);
|
|
await focusDatabaseHeader(page);
|
|
await expect(addColumnBtn).toBeVisible();
|
|
});
|
|
|
|
test('should support drag and drop to move columns', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page, 'column1');
|
|
await initDatabaseColumn(page, 'column2');
|
|
await initDatabaseColumn(page, 'column3');
|
|
|
|
const column1 = await focusDatabaseHeader(page, 1);
|
|
const moveIcon = column1.locator('.affine-database-column-move');
|
|
const moveIconBox = await getBoundingBox(moveIcon);
|
|
const x = moveIconBox.x + moveIconBox.width / 2;
|
|
const y = moveIconBox.y + moveIconBox.height / 2;
|
|
|
|
await dragBetweenCoords(
|
|
page,
|
|
{ x, y },
|
|
{ x: x + 100, y },
|
|
{
|
|
steps: 50,
|
|
beforeMouseUp: async () => {
|
|
await waitNextFrame(page);
|
|
const indicator = page.locator('.vertical-indicator').first();
|
|
await expect(indicator).toBeVisible();
|
|
|
|
const { box } = await getDatabaseHeaderColumn(page, 2);
|
|
const indicatorBox = await getBoundingBox(indicator);
|
|
expect(box.x + box.width - indicatorBox.x < 10).toBe(true);
|
|
},
|
|
}
|
|
);
|
|
|
|
const { text } = await getDatabaseHeaderColumn(page, 2);
|
|
expect(text).toBe('column1');
|
|
});
|
|
|
|
test('should title column support quick renaming', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await initDatabaseDynamicRowWithData(page, 'a', true);
|
|
await pressEscape(page);
|
|
await focusDatabaseHeader(page, 1);
|
|
const { textElement } = await getDatabaseHeaderColumn(page, 1);
|
|
await textElement.click();
|
|
await waitNextFrame(page);
|
|
await selectAllByKeyboard(page);
|
|
await type(page, '123');
|
|
await pressEnter(page);
|
|
expect(await textElement.innerText()).toBe('123');
|
|
|
|
await undoByClick(page);
|
|
expect(await textElement.innerText()).toBe('Column 1');
|
|
await textElement.click();
|
|
await waitNextFrame(page);
|
|
await selectAllByKeyboard(page);
|
|
await type(page, '123');
|
|
await pressEnter(page);
|
|
expect(await textElement.innerText()).toBe('123');
|
|
});
|
|
|
|
test('should title column support quick changing of column type', async ({
|
|
page,
|
|
}) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await initDatabaseDynamicRowWithData(page, 'a', true);
|
|
await pressEscape(page);
|
|
await initDatabaseDynamicRowWithData(page, 'b');
|
|
await pressEscape(page);
|
|
await focusDatabaseHeader(page, 1);
|
|
const { typeIcon } = await getDatabaseHeaderColumn(page, 1);
|
|
await typeIcon.click();
|
|
await waitNextFrame(page);
|
|
await clickColumnType(page, 'Select');
|
|
const cell = getFirstColumnCell(page, 'select-selected');
|
|
expect(await cell.count()).toBe(1);
|
|
});
|
|
|
|
test('database format-bar in header and text column', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await switchColumnType(page, 'Text');
|
|
await initDatabaseDynamicRowWithData(page, 'column', true);
|
|
await pressArrowLeft(page);
|
|
await pressEnter(page);
|
|
await type(page, 'header');
|
|
// Title | Column1
|
|
// ----------------
|
|
// header | column
|
|
|
|
const formatBar = getFormatBar(page);
|
|
await setInlineRangeInInlineEditor(page, { index: 1, length: 4 }, 1);
|
|
expect(await formatBar.formatBar.isVisible()).toBe(true);
|
|
// Title | Column1
|
|
// ----------------
|
|
// h|eade|r | column
|
|
|
|
await assertInlineEditorDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: 'header',
|
|
},
|
|
],
|
|
1
|
|
);
|
|
await formatBar.boldBtn.click();
|
|
await assertInlineEditorDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: 'h',
|
|
},
|
|
{
|
|
insert: 'eade',
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
{
|
|
insert: 'r',
|
|
},
|
|
],
|
|
1
|
|
);
|
|
|
|
await pressEscape(page);
|
|
await pressArrowRight(page);
|
|
await pressEnter(page);
|
|
await setInlineRangeInInlineEditor(page, { index: 2, length: 2 }, 2);
|
|
expect(await formatBar.formatBar.isVisible()).toBe(true);
|
|
// Title | Column1
|
|
// ----------------
|
|
// header | co|lu|mn
|
|
|
|
await assertInlineEditorDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: 'column',
|
|
},
|
|
],
|
|
2
|
|
);
|
|
await formatBar.boldBtn.click();
|
|
await assertInlineEditorDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: 'co',
|
|
},
|
|
{
|
|
insert: 'lu',
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
{
|
|
insert: 'mn',
|
|
},
|
|
],
|
|
2
|
|
);
|
|
});
|
|
|
|
test.describe('readonly mode', () => {
|
|
test('database title should not be edited in readonly mode', async ({
|
|
page,
|
|
}) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
const locator = page.locator('affine-database');
|
|
await expect(locator).toBeVisible();
|
|
|
|
const dbTitle = 'Database 1';
|
|
await assertBlockProps(page, '2', {
|
|
title: dbTitle,
|
|
});
|
|
|
|
await focusDatabaseTitle(page);
|
|
await selectAllByKeyboard(page);
|
|
await pressBackspace(page);
|
|
|
|
await type(page, 'hello');
|
|
await assertBlockProps(page, '2', {
|
|
title: 'hello',
|
|
});
|
|
|
|
await switchReadonly(page);
|
|
|
|
await type(page, ' world');
|
|
await assertBlockProps(page, '2', {
|
|
title: 'hello',
|
|
});
|
|
|
|
await pressBackspace(page, 'hello world'.length);
|
|
await assertBlockProps(page, '2', {
|
|
title: 'hello',
|
|
});
|
|
});
|
|
|
|
test('should rich-text not be edited in readonly mode', async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await switchColumnType(page, 'Text');
|
|
await initDatabaseDynamicRowWithData(page, '', true);
|
|
|
|
const cell = getFirstColumnCell(page, 'affine-database-rich-text');
|
|
await cell.click();
|
|
await type(page, '123');
|
|
await assertDatabaseCellRichTexts(page, { text: '123' });
|
|
|
|
await switchReadonly(page);
|
|
await pressBackspace(page);
|
|
await type(page, '789');
|
|
await assertDatabaseCellRichTexts(page, { text: '123' });
|
|
});
|
|
|
|
test('should hide edit widget after switch to readonly mode', async ({
|
|
page,
|
|
}) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await switchColumnType(page, 'Text');
|
|
await initDatabaseDynamicRowWithData(page, '', true);
|
|
|
|
const database = page.locator('affine-database');
|
|
await expect(database).toBeVisible();
|
|
|
|
const databaseMenu = database.locator('.database-ops');
|
|
await expect(databaseMenu).toBeVisible();
|
|
|
|
const addViewButton = database.getByTestId('database-add-view-button');
|
|
await expect(addViewButton).toBeVisible();
|
|
|
|
const titleHeader = page.locator('affine-database-header-column').filter({
|
|
hasText: 'Title',
|
|
});
|
|
await titleHeader.hover();
|
|
const columnDragBar = titleHeader.locator('.control-r');
|
|
await expect(columnDragBar).toBeVisible();
|
|
|
|
const filter = database.locator('data-view-header-tools-filter');
|
|
const search = database.locator('data-view-header-tools-search');
|
|
const options = database.locator('data-view-header-tools-view-options');
|
|
const headerAddRow = database.locator('data-view-header-tools-add-row');
|
|
|
|
await database.hover();
|
|
await expect(filter).toBeVisible();
|
|
await expect(search).toBeVisible();
|
|
await expect(options).toBeVisible();
|
|
await expect(headerAddRow).toBeVisible();
|
|
|
|
const row = database.locator('data-view-table-row');
|
|
const rowOptions = row.locator('.row-op');
|
|
const rowDragBar = row.locator('.data-view-table-view-drag-handler>div');
|
|
await row.hover();
|
|
await expect(rowOptions).toHaveCount(2);
|
|
await expect(rowOptions.nth(0)).toBeVisible();
|
|
await expect(rowOptions.nth(1)).toBeVisible();
|
|
await expect(rowDragBar).toBeVisible();
|
|
|
|
const addRow = database.locator('.data-view-table-group-add-row');
|
|
await expect(addRow).toBeVisible();
|
|
|
|
// Readonly Mode
|
|
{
|
|
await switchReadonly(page);
|
|
await expect(databaseMenu).toBeHidden();
|
|
await expect(addViewButton).toBeHidden();
|
|
|
|
await titleHeader.hover();
|
|
await expect(columnDragBar).toBeHidden();
|
|
|
|
await database.hover();
|
|
await expect(filter).toBeHidden();
|
|
await expect(search).toBeVisible(); // Note the search should not be hidden
|
|
await expect(options).toBeHidden();
|
|
await expect(headerAddRow).toBeHidden();
|
|
|
|
await row.hover();
|
|
await expect(rowOptions.nth(0)).toBeHidden();
|
|
await expect(rowOptions.nth(1)).toBeHidden();
|
|
await expect(rowDragBar).toBeHidden();
|
|
|
|
await expect(addRow).toBeHidden();
|
|
}
|
|
});
|
|
|
|
test('should hide focus border after switch to readonly mode', async ({
|
|
page,
|
|
}) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await switchColumnType(page, 'Text');
|
|
await initDatabaseDynamicRowWithData(page, '', true);
|
|
|
|
const database = page.locator('affine-database');
|
|
await expect(database).toBeVisible();
|
|
|
|
const cell = getFirstColumnCell(page, 'affine-database-rich-text');
|
|
await cell.click();
|
|
|
|
const focusBorder = database.locator(
|
|
'data-view-table-selection .database-focus'
|
|
);
|
|
await expect(focusBorder).toBeVisible();
|
|
|
|
await switchReadonly(page);
|
|
await expect(focusBorder).toBeHidden();
|
|
});
|
|
|
|
test('should hide selection after switch to readonly mode', async ({
|
|
page,
|
|
}) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyDatabaseState(page);
|
|
|
|
await initDatabaseColumn(page);
|
|
await switchColumnType(page, 'Text');
|
|
await initDatabaseDynamicRowWithData(page, '', true);
|
|
|
|
const database = page.locator('affine-database');
|
|
await expect(database).toBeVisible();
|
|
|
|
const startCell = getDatabaseBodyCell(page, {
|
|
rowIndex: 0,
|
|
columnIndex: 0,
|
|
});
|
|
const endCell = getDatabaseBodyCell(page, {
|
|
rowIndex: 0,
|
|
columnIndex: 1,
|
|
});
|
|
|
|
const startBox = await getBoundingBox(startCell);
|
|
const endBox = await getBoundingBox(endCell);
|
|
|
|
const startX = startBox.x + startBox.width / 2;
|
|
const startY = startBox.y + startBox.height / 2;
|
|
const endX = endBox.x + endBox.width / 2;
|
|
const endY = endBox.y + endBox.height / 2;
|
|
|
|
await dragBetweenCoords(
|
|
page,
|
|
{ x: startX, y: startY },
|
|
{ x: endX, y: endY }
|
|
);
|
|
|
|
const selection = database.locator(
|
|
'data-view-table-selection .database-selection'
|
|
);
|
|
|
|
await expect(selection).toBeVisible();
|
|
|
|
await switchReadonly(page);
|
|
await expect(selection).toBeHidden();
|
|
});
|
|
});
|