Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Playwright tests #337

Merged
merged 48 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
669ffd0
setup playwright
BrtqKr Apr 15, 2024
9351ef7
setup multiple browsers
BrtqKr Apr 15, 2024
6cfb39e
fix style tests, fix multiple browsers setup
BrtqKr Apr 17, 2024
dcf45fd
add paste test
BrtqKr Apr 18, 2024
850215d
cleanup
BrtqKr Apr 18, 2024
12ac98d
add paste replace and quick type
BrtqKr Apr 18, 2024
c9fc4b8
fix paste permissions in tests
BrtqKr Apr 18, 2024
72f12ef
cut content wip
BrtqKr Apr 19, 2024
8ef9df1
add cut content test
BrtqKr Apr 22, 2024
b2452bb
fix test permissions
BrtqKr Apr 22, 2024
906b98b
add undo test, add debounce value to constants
BrtqKr Apr 23, 2024
abbaff1
redo wip
BrtqKr Apr 23, 2024
f661556
finish redo
BrtqKr Apr 23, 2024
bb58135
cleanup commands
BrtqKr Apr 23, 2024
67a2e9d
cleanup tests structure
BrtqKr Apr 23, 2024
124d151
add blockquote style
BrtqKr Apr 24, 2024
690ddc1
fix blockquote test for firefox
BrtqKr Apr 24, 2024
ac548f7
Merge remote-tracking branch 'origin/main' into brtqkr/add-playwright…
BrtqKr Apr 28, 2024
1dd1977
cleanup
BrtqKr Apr 28, 2024
e26e01f
cleanup
BrtqKr Apr 28, 2024
afa5758
add workflow for e2e test
BrtqKr May 6, 2024
9275353
cleanup, rename constants
BrtqKr May 6, 2024
4286e8e
review fixes
BrtqKr May 6, 2024
9d63b1c
Merge remote-tracking branch 'origin/main' into brtqkr/add-playwright…
BrtqKr May 6, 2024
d0e36b7
update workflow
BrtqKr May 6, 2024
721b2ca
update workflow
BrtqKr May 6, 2024
f12b729
fix home variable
BrtqKr May 6, 2024
e92e31b
tests/textManipulation.spec.ts fix attempt
BrtqKr May 6, 2024
9e1464f
trigger playwright from node modules path
BrtqKr May 6, 2024
7e28ce8
change working dir
BrtqKr May 6, 2024
a702f31
revert to image
BrtqKr May 6, 2024
94e22d6
cleanup
BrtqKr May 6, 2024
9f4d9c3
cleanup
BrtqKr May 6, 2024
3a065f2
cleanup
BrtqKr May 6, 2024
5b06cd2
cleanup
BrtqKr May 6, 2024
7e3e85a
revert to image
BrtqKr May 6, 2024
4daf5aa
cleanup
BrtqKr May 7, 2024
085d094
cleanup
BrtqKr May 9, 2024
abc897a
add ci disable for tests failing with ubuntu-runner
BrtqKr May 9, 2024
94d7ddb
add ci disable for tests failing with ubuntu-runner
BrtqKr May 9, 2024
2764945
fix jest test paths
BrtqKr May 9, 2024
c44a707
review fixes wip
BrtqKr May 13, 2024
a0a1bf2
review fixes wip
BrtqKr May 13, 2024
72e700f
remove constants
BrtqKr May 14, 2024
cd88a46
cleanup
BrtqKr May 14, 2024
b04d1f1
lint
BrtqKr May 14, 2024
3c6d829
fix test paths
BrtqKr May 14, 2024
d612c93
Merge remote-tracking branch 'origin/main' into brtqkr/add-playwright…
BrtqKr Jun 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ module.exports = {
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/array-type': ['error', {default: 'array-simple'}],
'@typescript-eslint/consistent-type-definitions': 'off',
'curly': ['error', 'all'],
},
};
49 changes: 49 additions & 0 deletions .github/workflows/web-e2e-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Test web E2E
on:
pull_request:
paths:
- .github/workflows/web-e2e-test.yml
- src/**
- WebExample/**
merge_group:
branches:
- main
push:
branches:
- main
paths:
- .github/workflows/web-e2e-test.yml
- src/**
- WebExample/**

jobs:
test:
if: github.repository == 'Expensify/react-native-live-markdown'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./WebExample

concurrency:
group: web-e2e-test-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Check out Git repository
uses: actions/checkout@v4

- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18

- name: Install node_modules
run: yarn install --immutable

- name: Install browsers
run: npx playwright install --with-deps

- name: Install dependencies for browsers
run: npx playwright install-deps

- name: Run Playwright tests
run: yarn test
4 changes: 4 additions & 0 deletions WebExample/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ yarn-error.*

# typescript
*.tsbuildinfo
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
6 changes: 6 additions & 0 deletions WebExample/__tests__/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
rules: {
'@lwc/lwc/no-async-await': 'off',
'rulesdir/prefer-import-module-contents': 'off',
},
};
32 changes: 32 additions & 0 deletions WebExample/__tests__/input.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {test, expect} from '@playwright/test';
import * as TEST_CONST from './testConstants';
import {checkCursorPosition, setupInput} from './utils';

test.beforeEach(async ({page}) => {
await page.goto(TEST_CONST.LOCAL_URL, {waitUntil: 'load'});
});

test.describe('typing', () => {
test('short text', async ({page}) => {
const inputLocator = await setupInput(page, 'clear');

await inputLocator.focus();
await inputLocator.pressSequentially(TEST_CONST.EXAMPLE_CONTENT);
const value = await inputLocator.innerText();
expect(value).toEqual(TEST_CONST.EXAMPLE_CONTENT);
});

test('fast type cursor position', async ({page}) => {
const EXAMPLE_LONG_CONTENT = TEST_CONST.EXAMPLE_CONTENT.repeat(3);

const inputLocator = await setupInput(page, 'clear');

await inputLocator.pressSequentially(EXAMPLE_LONG_CONTENT);

expect(await inputLocator.innerText()).toBe(EXAMPLE_LONG_CONTENT);

const cursorPosition = await page.evaluate(checkCursorPosition);

expect(cursorPosition).toBe(EXAMPLE_LONG_CONTENT.length);
});
});
62 changes: 62 additions & 0 deletions WebExample/__tests__/styles.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {test, expect} from '@playwright/test';
import type {Page} from '@playwright/test';
import * as TEST_CONST from './testConstants';
import {setupInput, getElementStyle} from './utils';

const testMarkdownContentStyle = async ({testContent, style, page}: {testContent: string; style: string; page: Page}) => {
const inputLocator = await setupInput(page);

const elementHandle = inputLocator.locator('span', {hasText: testContent}).last();
const elementStyle = await getElementStyle(elementHandle);

expect(elementStyle).toEqual(style);
};

test.beforeEach(async ({page}) => {
await page.goto(TEST_CONST.LOCAL_URL, {waitUntil: 'load'});
await page.click('[data-testid="reset"]');
});

test.describe('markdown content styling', () => {
test('bold', async ({page}) => {
await testMarkdownContentStyle({testContent: 'world', style: 'font-weight: bold;', page});
});

test('link', async ({page}) => {
await testMarkdownContentStyle({testContent: 'https://expensify.com', style: 'color: blue; text-decoration: underline;', page});
});

test('h1', async ({page}) => {
await testMarkdownContentStyle({testContent: 'header1', style: 'font-size: 25px; font-weight: bold;', page});
});

test('inline code', async ({page}) => {
await testMarkdownContentStyle({testContent: 'inline code', style: 'font-family: monospace; font-size: 20px; color: black; background-color: lightgray;', page});
});

test('codeblock', async ({page}) => {
await testMarkdownContentStyle({testContent: 'codeblock', style: 'font-family: monospace; font-size: 20px; color: black; background-color: lightgray;', page});
});

test('mention-here', async ({page}) => {
await testMarkdownContentStyle({testContent: 'here', style: 'color: green; background-color: lime;', page});
});

test('mention-user', async ({page}) => {
await testMarkdownContentStyle({testContent: '[email protected]', style: 'color: blue; background-color: cyan;', page});
});

test('mention-report', async ({page}) => {
await testMarkdownContentStyle({testContent: 'mention-report', style: 'color: red; background-color: pink;', page});
});

test('blockquote', async ({page, browserName}) => {
const blockquoteStyle =
'border-color: gray; border-width: 6px; margin-left: 6px; padding-left: 6px; border-left-style: solid; display: inline-block; max-width: 100%; box-sizing: border-box;';

// Firefox border properties are serialized slightly differently
const browserStyle = browserName === 'firefox' ? blockquoteStyle.replace('border-left-style: solid', 'border-left: 6px solid gray') : blockquoteStyle;

await testMarkdownContentStyle({testContent: 'blockquote', style: browserStyle, page});
});
});
18 changes: 18 additions & 0 deletions WebExample/__tests__/testConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const LOCAL_URL = 'http://localhost:19006/';

const EXAMPLE_CONTENT = [
'Hello, *world*!',
'https://expensify.com',
'# header1',
'> blockquote',
'`inline code`',
'```\ncodeblock\n```',
'@here',
'@[email protected]',
'#mention-report',
].join('\n');

const INPUT_ID = 'MarkdownInput_Example';
const INPUT_HISTORY_DEBOUNCE_TIME_MS = 150;

export {LOCAL_URL, EXAMPLE_CONTENT, INPUT_ID, INPUT_HISTORY_DEBOUNCE_TIME_MS};
135 changes: 135 additions & 0 deletions WebExample/__tests__/textManipulation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {test, expect} from '@playwright/test';
import type {Locator, Page} from '@playwright/test';
import * as TEST_CONST from './testConstants';
import {checkCursorPosition, setupInput, getElementStyle, pressCmd} from './utils';

const pasteContent = async ({text, page, inputLocator}: {text: string; page: Page; inputLocator: Locator}) => {
await page.evaluate(async (pasteText) => navigator.clipboard.writeText(pasteText), text);
await inputLocator.focus();
await pressCmd({inputLocator, command: 'v'});
};

test.beforeEach(async ({page, context, browserName}) => {
await page.goto(TEST_CONST.LOCAL_URL, {waitUntil: 'load'});
if (browserName === 'chromium') {
await context.grantPermissions(['clipboard-write', 'clipboard-read']);
}
});

test.describe('paste content', () => {
test.skip(({browserName}) => !!process.env.CI && browserName === 'webkit', 'Excluded from WebKit CI tests');

test('paste', async ({page}) => {
const PASTE_TEXT = 'bold';
const BOLD_STYLE = 'font-weight: bold;';

const inputLocator = await setupInput(page, 'clear');

const wrappedText = '*bold*';
await pasteContent({text: wrappedText, page, inputLocator});

const elementHandle = await inputLocator.locator('span', {hasText: PASTE_TEXT}).last();
const elementStyle = await getElementStyle(elementHandle);

expect(elementStyle).toEqual(BOLD_STYLE);
});

test('paste replace', async ({page}) => {
const inputLocator = await setupInput(page, 'reset');

await inputLocator.focus();
await pressCmd({inputLocator, command: 'a'});

const newText = '*bold*';
await pasteContent({text: newText, page, inputLocator});

expect(await inputLocator.innerText()).toBe(newText);
});

test('paste undo', async ({page, browserName}) => {
test.skip(!!process.env.CI && browserName === 'firefox', 'Excluded from Firefox CI tests');

const PASTE_TEXT_FIRST = '*bold*';
const PASTE_TEXT_SECOND = '@here';

const inputLocator = await setupInput(page, 'clear');

await page.evaluate(async (pasteText) => navigator.clipboard.writeText(pasteText), PASTE_TEXT_FIRST);

await pressCmd({inputLocator, command: 'v'});
await page.waitForTimeout(TEST_CONST.INPUT_HISTORY_DEBOUNCE_TIME_MS);
await page.evaluate(async (pasteText) => navigator.clipboard.writeText(pasteText), PASTE_TEXT_SECOND);
await pressCmd({inputLocator, command: 'v'});
await page.waitForTimeout(TEST_CONST.INPUT_HISTORY_DEBOUNCE_TIME_MS);

await pressCmd({inputLocator, command: 'z'});

expect(await inputLocator.innerText()).toBe(PASTE_TEXT_FIRST);
});

test('paste redo', async ({page}) => {
const PASTE_TEXT_FIRST = '*bold*';
const PASTE_TEXT_SECOND = '@here';

const inputLocator = await setupInput(page, 'clear');

await page.evaluate(async (pasteText) => navigator.clipboard.writeText(pasteText), PASTE_TEXT_FIRST);
await pressCmd({inputLocator, command: 'v'});
await page.waitForTimeout(TEST_CONST.INPUT_HISTORY_DEBOUNCE_TIME_MS);
await page.evaluate(async (pasteText) => navigator.clipboard.writeText(pasteText), PASTE_TEXT_SECOND);
await page.waitForTimeout(TEST_CONST.INPUT_HISTORY_DEBOUNCE_TIME_MS);
await pressCmd({inputLocator, command: 'v'});
await page.waitForTimeout(TEST_CONST.INPUT_HISTORY_DEBOUNCE_TIME_MS);

await pressCmd({inputLocator, command: 'z'});
await pressCmd({inputLocator, command: 'Shift+z'});

expect(await inputLocator.innerText()).toBe(`${PASTE_TEXT_FIRST}${PASTE_TEXT_SECOND}`);
});
});

test('select all', async ({page}) => {
const inputLocator = await setupInput(page, 'reset');
await inputLocator.focus();
await pressCmd({inputLocator, command: 'a'});

const cursorPosition = await page.evaluate(checkCursorPosition);

expect(cursorPosition).toBe(TEST_CONST.EXAMPLE_CONTENT.length);
});

test('cut content changes', async ({page, browserName}) => {
test.skip(!!process.env.CI && browserName === 'webkit', 'Excluded from WebKit CI tests');

const INITIAL_CONTENT = 'bold';
const WRAPPED_CONTENT = `*${INITIAL_CONTENT}*`;
const EXPECTED_CONTENT = WRAPPED_CONTENT.slice(0, 3);

const inputLocator = await setupInput(page, 'clear');
await pasteContent({text: WRAPPED_CONTENT, page, inputLocator});
const rootHandle = await inputLocator.locator('span.root').first();

await page.evaluate(async (initialContent) => {
const filteredNode = Array.from(document.querySelectorAll('div[contenteditable="true"] > span.root span')).find((node) => {
return node.textContent?.includes(initialContent) && node.nextElementSibling && node.nextElementSibling.textContent?.includes('*');
});

const startNode = filteredNode;
const endNode = filteredNode?.nextElementSibling;

if (startNode?.firstChild && endNode?.lastChild) {
const range = new Range();
range.setStart(startNode.firstChild, 2);
range.setEnd(endNode.lastChild, endNode.lastChild.textContent?.length ?? 0);

const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
}, INITIAL_CONTENT);

await inputLocator.focus();
await pressCmd({inputLocator, command: 'x'});

expect(await rootHandle.innerHTML()).toBe(EXPECTED_CONTENT);
});
58 changes: 58 additions & 0 deletions WebExample/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type {Locator, Page} from '@playwright/test';
import * as TEST_CONST from './testConstants';

const setupInput = async (page: Page, action?: 'clear' | 'reset') => {
const inputLocator = await page.locator(`div#${TEST_CONST.INPUT_ID}`);
if (action) {
await page.click(`[data-testid="${action}"]`);
}

return inputLocator;
};

const checkCursorPosition = () => {
const editableDiv = document.querySelector('div[contenteditable="true"]') as HTMLElement;
const range = window.getSelection()?.getRangeAt(0);
if (!range || !editableDiv) {
return null;
}
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(editableDiv);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
};

const setCursorPosition = ({startNode, endNode}: {startNode?: Element; endNode?: Element | null}) => {
if (!startNode?.firstChild || !endNode?.lastChild) {
return null;
}

const range = new Range();
range.setStart(startNode.firstChild, 2);
range.setEnd(endNode.lastChild, endNode.lastChild.textContent?.length ?? 0);

const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);

return selection;
};

const getElementStyle = async (elementHandle: Locator) => {
let elementStyle;

if (elementHandle) {
await elementHandle.waitFor({state: 'attached'});

elementStyle = await elementHandle.getAttribute('style');
}
return elementStyle;
};

const pressCmd = async ({inputLocator, command}: {inputLocator: Locator; command: string}) => {
const OPERATION_MODIFIER = process.platform === 'darwin' ? 'Meta' : 'Control';

await inputLocator.press(`${OPERATION_MODIFIER}+${command}`);
};

export {setupInput, checkCursorPosition, setCursorPosition, getElementStyle, pressCmd};
Loading
Loading