Skip to content

Commit 6b5187f

Browse files
committed
test: added basic clipboard tests for wysiwyg
1 parent 6ab3b98 commit 6b5187f

File tree

8 files changed

+317
-706
lines changed

8 files changed

+317
-706
lines changed

package-lock.json

+154-696
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bundle/settings/index.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export const EditorSettings = memo<EditorSettingsProps>(function EditorSettings(
7575
pin="round-round"
7676
onClick={togglePopup}
7777
ref={setChevronElement}
78+
qa="md-settings-button"
7879
className={bSettings('dropdown-button')}
7980
>
8081
<Icon data={Gear} />
@@ -125,9 +126,10 @@ const SettingsContent: React.FC<SettingsContentProps> = function SettingsContent
125126
showPreview,
126127
}) {
127128
return (
128-
<div className={bContent(null, [className])}>
129+
<div className={bContent(null, [className])} data-qa="md-settings-content">
129130
<Menu size="l" className={bContent('mode')}>
130131
<Menu.Item
132+
qa="md-settings-mode-wysiwyg"
131133
active={mode === 'wysiwyg'}
132134
onClick={() => {
133135
onModeChange('wysiwyg');
@@ -138,6 +140,7 @@ const SettingsContent: React.FC<SettingsContentProps> = function SettingsContent
138140
{i18n('settings_wysiwyg')}
139141
</Menu.Item>
140142
<Menu.Item
143+
qa="md-settings-mode-markup"
141144
active={mode === 'markup'}
142145
onClick={() => {
143146
onModeChange('markup');

tests/playwright/core/editor.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type {Expect, Page} from '@playwright/test';
2+
3+
import type {DataTransferType, MarkdownEditorMode} from 'src';
4+
5+
class MarkdownEditorLocators {
6+
readonly component;
7+
readonly editor;
8+
9+
readonly settingsButton;
10+
readonly settingsContent;
11+
12+
readonly contenteditable;
13+
14+
constructor(page: Page) {
15+
this.component = page.locator('.g-md-editor-component');
16+
this.editor = page.locator('.g-md-editor-component__editor');
17+
18+
this.settingsButton = page.getByTestId('md-settings-button');
19+
this.settingsContent = page.getByTestId('md-settings-content');
20+
21+
this.contenteditable = this.editor.locator('[contenteditable=true]');
22+
}
23+
}
24+
25+
type PasteData = Partial<Record<DataTransferType, string>>;
26+
27+
export class MarkdownEditorPage {
28+
protected readonly page: Page;
29+
protected readonly expect: Expect;
30+
protected readonly locators;
31+
32+
constructor(page: Page, expect: Expect) {
33+
this.page = page;
34+
this.expect = expect;
35+
36+
this.locators = new MarkdownEditorLocators(page);
37+
}
38+
39+
async getMode(): Promise<MarkdownEditorMode> {
40+
return await this.locators.editor.evaluate((element) => {
41+
if (element.classList.contains('g-md-editor-component__editor_mode_wysiwyg')) {
42+
return 'wysiwyg';
43+
}
44+
45+
if (element.classList.contains('g-md-editor-component__editor_mode_markup')) {
46+
return 'markup';
47+
}
48+
49+
throw new Error('MarkdownEditorPage.getMode(): unknown editor mode');
50+
});
51+
}
52+
53+
async openSettingsPopup() {
54+
if (await this.locators.settingsContent.isVisible()) return;
55+
56+
await this.locators.settingsButton.click();
57+
await this.locators.settingsContent.waitFor({state: 'visible'});
58+
}
59+
60+
async setMode(mode: MarkdownEditorMode) {
61+
if ((await this.getMode()) === mode) {
62+
return;
63+
}
64+
65+
await this.openSettingsPopup();
66+
await this.locators.settingsContent.getByTestId(`md-settings-mode-${mode}`).click();
67+
await this.assertMode(mode);
68+
}
69+
70+
async assertMode(mode: MarkdownEditorMode) {
71+
await this.expect.poll(() => this.getMode()).toBe(mode);
72+
}
73+
74+
async blur() {
75+
await this.locators.contenteditable.blur();
76+
}
77+
78+
async clearContent() {
79+
await this.locators.contenteditable.press('ControlOrMeta+A');
80+
await this.locators.contenteditable.press('Backspace');
81+
}
82+
83+
async paste(data: PasteData) {
84+
await this.locators.contenteditable.evaluate((element, data) => {
85+
const clipboardData = new DataTransfer();
86+
87+
for (const [key, value] of Object.entries(data)) {
88+
clipboardData.setData(key, value);
89+
}
90+
91+
element.focus();
92+
element.dispatchEvent(new ClipboardEvent('paste', {clipboardData}));
93+
}, data);
94+
}
95+
}

tests/playwright/core/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {test as base} from '@playwright/experimental-ct-react';
1+
import {test as base, expect} from '@playwright/experimental-ct-react';
22

3+
import {MarkdownEditorPage} from './editor';
34
import {expectScreenshot} from './expectScreenshot';
45
import {mount} from './mount';
56
import type {Fixtures} from './types';
@@ -9,6 +10,10 @@ export const test = base.extend<Fixtures>({
910
mount,
1011
expectScreenshot,
1112
wait,
13+
editor: async ({page}, use) => {
14+
const editor = new MarkdownEditorPage(page, expect);
15+
await use(editor);
16+
},
1217
});
1318

1419
export {expect} from '@playwright/experimental-ct-react';

tests/playwright/core/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {
1212
TestFixture,
1313
} from '@playwright/test';
1414

15+
import type {MarkdownEditorPage} from './editor';
16+
1517
interface ComponentFixtures {
1618
mount<HooksConfig>(
1719
component: JSX.Element,
@@ -31,6 +33,7 @@ export type Fixtures = {
3133
mount: MountFixture;
3234
expectScreenshot: ExpectScreenshotFixture;
3335
wait: WaitFixture;
36+
editor: MarkdownEditorPage;
3437
};
3538

3639
export type MountFixture = ComponentFixtures['mount'];

tests/playwright/playwright.config.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import {resolve} from 'node:path';
22

33
import type {PlaywrightTestConfig} from '@playwright/experimental-ct-react';
44
import {defineConfig, devices} from '@playwright/experimental-ct-react';
5+
import type {InlineConfig} from 'vite'; // eslint-disable-line import/no-extraneous-dependencies
56

6-
function pathFromRoot(p: string) {
7+
import tsConfig from '../../tsconfig.json';
8+
9+
const pathFromRoot = (p: string) => {
710
return resolve(__dirname, '../', p);
8-
}
11+
};
912

1013
const reporter: PlaywrightTestConfig['reporter'] = [];
1114

@@ -23,7 +26,19 @@ reporter.push(
2326
],
2427
);
2528

26-
const ctViteConfig = {
29+
const aliasesFromTsConf = (() => {
30+
const baseUrl = resolve(__dirname, '..', '..', tsConfig.compilerOptions.baseUrl);
31+
const paths = tsConfig.compilerOptions.paths;
32+
33+
return Object.entries(paths).reduce<Record<string, string>>((acc, [key, value]) => {
34+
const cleanKey = key.replace('/*', '');
35+
const cleanValue = value[0].replace('/*', '');
36+
acc[cleanKey] = resolve(baseUrl, cleanValue);
37+
return acc;
38+
}, {});
39+
})();
40+
41+
const ctViteConfig: InlineConfig = {
2742
css: {
2843
preprocessorOptions: {
2944
scss: {
@@ -33,11 +48,7 @@ const ctViteConfig = {
3348
},
3449
resolve: {
3550
alias: {
36-
'#core': resolve(__dirname, '../../src/core'),
37-
'#cm': resolve(__dirname, '../../src/cm'),
38-
'#pm': resolve(__dirname, '../../src/pm'),
39-
src: resolve(__dirname, '../../src'),
40-
playwright: resolve(__dirname),
51+
...aliasesFromTsConf,
4152
'~@gravity-ui/uikit/styles/mixins': '@gravity-ui/uikit/styles/mixins',
4253
},
4354
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {test} from 'playwright/core';
2+
3+
import {Playground} from './Playground.helpers';
4+
5+
test.describe('Clipboard', () => {
6+
test('should wysiwyg parse markdown markup when pasting', async ({
7+
mount,
8+
expectScreenshot,
9+
editor,
10+
}) => {
11+
await mount(<Playground />);
12+
13+
await editor.assertMode('wysiwyg');
14+
await editor.clearContent();
15+
16+
await editor.paste({
17+
'text/plain': 'Lorem ipsum dolor sit amet',
18+
'text/yfm': '## Lorem *ipsum* **dolor** ~~sit~~ amet',
19+
});
20+
21+
await expectScreenshot();
22+
});
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {composeStories} from '@storybook/react';
2+
3+
import * as DefaultPlaygroundStories from '../../../demo/stories/playground/Playground.stories';
4+
5+
const PlaygroundStories = composeStories(DefaultPlaygroundStories, {
6+
argsEnhancers: [
7+
() => ({
8+
stickyToolbar: false,
9+
}),
10+
],
11+
});
12+
13+
export const Playground = PlaygroundStories.Story;

0 commit comments

Comments
 (0)