Skip to content

Commit c2b5b5b

Browse files
🪄 add undo commits on enter and space (#3)
🪄 add undo commits on enter and space (#3)
2 parents f161aff + 8be7d5e commit c2b5b5b

File tree

7 files changed

+145
-55
lines changed

7 files changed

+145
-55
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: CI
33
on:
44
push:
55
branches:
6-
- '**'
6+
- "**"
77

88
jobs:
99
build:
@@ -23,9 +23,16 @@ jobs:
2323

2424
- name: Install Playwright Browsers
2525
run: npx playwright install chromium
26-
26+
2727
# note: we need to build before running tests because it's a chrome extension
2828
- name: Test
2929
run: |
3030
yarn build
31-
yarn test:e2e
31+
yarn test:e2e
32+
33+
- uses: actions/upload-artifact@v4
34+
if: ${{ !cancelled() }}
35+
with:
36+
name: playwright-report
37+
path: playwright-report/
38+
retention-days: 30

playwright.config.ts

Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineConfig, devices } from '@playwright/test';
1+
import { defineConfig, devices } from "@playwright/test";
22

33
/**
44
* Read environment variables from file.
@@ -12,68 +12,27 @@ import { defineConfig, devices } from '@playwright/test';
1212
* See https://playwright.dev/docs/test-configuration.
1313
*/
1414
export default defineConfig({
15-
testDir: 'tests',
15+
testDir: "tests",
1616
/* Run tests in files in parallel */
1717
fullyParallel: true,
1818
/* Fail the build on CI if you accidentally left test.only in the source code. */
1919
forbidOnly: !!process.env.CI,
2020
/* Retry on CI only */
2121
retries: process.env.CI ? 2 : 0,
22-
/* Opt out of parallel tests on CI. */
2322
workers: process.env.CI ? 1 : undefined,
2423
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
25-
reporter: 'list',
24+
reporter: [["html", { open: "never" }], ["list"]],
2625
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
2726
use: {
28-
/* Base URL to use in actions like `await page.goto('/')`. */
29-
// baseURL: 'http://127.0.0.1:3000',
30-
3127
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
32-
trace: 'on-first-retry',
28+
trace: "on-first-retry",
3329
},
3430

3531
/* Configure projects for major browsers */
3632
projects: [
3733
{
38-
name: 'chromium',
39-
use: { ...devices['Desktop Chrome'] },
34+
name: "chromium",
35+
use: { ...devices["Desktop Chrome"] },
4036
},
41-
42-
// {
43-
// name: 'firefox',
44-
// use: { ...devices['Desktop Firefox'] },
45-
// },
46-
47-
// {
48-
// name: 'webkit',
49-
// use: { ...devices['Desktop Safari'] },
50-
// },
51-
52-
/* Test against mobile viewports. */
53-
// {
54-
// name: 'Mobile Chrome',
55-
// use: { ...devices['Pixel 5'] },
56-
// },
57-
// {
58-
// name: 'Mobile Safari',
59-
// use: { ...devices['iPhone 12'] },
60-
// },
61-
62-
/* Test against branded browsers. */
63-
// {
64-
// name: 'Microsoft Edge',
65-
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
66-
// },
67-
// {
68-
// name: 'Google Chrome',
69-
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
70-
// },
7137
],
72-
73-
/* Run your local dev server before starting the tests */
74-
// webServer: {
75-
// command: 'npm run start',
76-
// url: 'http://127.0.0.1:3000',
77-
// reuseExistingServer: !process.env.CI,
78-
// },
7938
});

src/lib/extensions/CommitUndo.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Editor, Extension } from "@tiptap/core";
2+
import { yUndoPluginKey } from "y-prosemirror";
3+
import { UndoManager } from "yjs";
4+
5+
const commitUndo = (editor: Editor) => {
6+
const state = editor.state;
7+
const undoManager: UndoManager = yUndoPluginKey.getState(state).undoManager;
8+
undoManager.stopCapturing();
9+
};
10+
11+
export const CommitUndo = Extension.create({
12+
name: "commitOnEnter",
13+
14+
addKeyboardShortcuts() {
15+
return {
16+
Enter: ({ editor }) => {
17+
commitUndo(editor);
18+
return false;
19+
},
20+
Space: ({ editor }) => {
21+
commitUndo(editor);
22+
return false;
23+
},
24+
};
25+
},
26+
});
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { nodePasteRule, type PasteRuleFinder } from "@tiptap/core";
1010
import * as Y from "yjs";
1111
import Collaboration from "@tiptap/extension-collaboration";
1212
import Underline from "@tiptap/extension-underline";
13-
import TextAlign from '@tiptap/extension-text-align'
13+
import TextAlign from "@tiptap/extension-text-align";
14+
import { CommitUndo } from "./CommitUndo";
1415

1516
export const loadExtensions = (doc: Y.Doc) => {
1617
const ImageFinder: PasteRuleFinder = /data:image\//g;
@@ -35,6 +36,7 @@ export const loadExtensions = (doc: Y.Doc) => {
3536
});
3637

3738
const lowlight = createLowlight(common);
39+
console.log(doc);
3840

3941
// define your extension array
4042
const extensions = [
@@ -58,8 +60,9 @@ export const loadExtensions = (doc: Y.Doc) => {
5860
document: doc, // Configure Y.Doc for collaboration
5961
}),
6062
TextAlign.configure({
61-
types: ['heading', 'paragraph']
62-
})
63+
types: ["heading", "paragraph"],
64+
}),
65+
CommitUndo,
6366
];
6467

6568
return extensions;

tests/commit.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { test, expect, getPreparedTab, equals } from "./fixtures";
2+
import { loremIpsum } from "lorem-ipsum";
3+
4+
// skipped because of https://github.com/microsoft/playwright/issues/33832
5+
test.skip("Ctrl+Z should only undo the last line after Enter", async ({
6+
context,
7+
}) => {
8+
const tab = await getPreparedTab(context);
9+
const input = await tab.getByRole("textbox");
10+
// remove all content
11+
await input.fill("");
12+
13+
// Generate two sentences of text
14+
const firstLine = loremIpsum({ count: 1, units: "words" });
15+
const secondLine = loremIpsum({ count: 1, units: "words" });
16+
17+
// Type the first line and press Enter
18+
await input.pressSequentially(firstLine, { delay: 10 });
19+
await tab.keyboard.press("Enter");
20+
21+
// Type the second line
22+
await input.pressSequentially(secondLine, { delay: 10 });
23+
24+
// Verify both lines are present in the textbox
25+
await equals(input, `${firstLine}\n\n${secondLine}`);
26+
27+
// Undo the last line by pressing Ctrl+Z
28+
await tab.keyboard.press('ControlOrMeta+Z');
29+
30+
// Verify the second line was removed
31+
await equals(input, firstLine);
32+
});
33+
34+
// skipped because of https://github.com/microsoft/playwright/issues/33832
35+
test.skip("Ctrl+Z should only undo the last word after Space", async ({
36+
context,
37+
}) => {
38+
const tab = await getPreparedTab(context);
39+
const input = await tab.getByRole("textbox");
40+
// remove all content
41+
await input.fill("");
42+
43+
// Generate two words of text
44+
const firstWord = loremIpsum({ count: 1, units: "words" });
45+
const secondWord = loremIpsum({ count: 1, units: "words" });
46+
47+
// Type the first word and press Space
48+
await input.pressSequentially(firstWord, { delay: 10 });
49+
await tab.keyboard.press("Space");
50+
51+
// Type the second word
52+
await input.pressSequentially(secondWord, { delay: 10 });
53+
54+
// Verify both words are present in the textbox
55+
await equals(input, `${firstWord} ${secondWord}`);
56+
57+
// Undo the last word by pressing Ctrl+Z
58+
await tab.keyboard.press('ControlOrMeta+Z');
59+
60+
// Verify the second word was removed
61+
await equals(input, firstWord);
62+
});

tests/fixtures.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,35 @@ export const test = base.extend<ExtensionFixtures>({
4646
});
4747

4848
export { expect } from "@playwright/test";
49+
50+
import { Locator } from '@playwright/test';
51+
52+
/**
53+
* Verifies that the Tiptap editor content contains the expected text.
54+
*
55+
* @param editorLocator - The Playwright locator for the editor element.
56+
* @param expectedText - The text to verify is contained within the editor.
57+
* @throws Error if the expected text is not found in the editor content.
58+
*/
59+
export async function contains(editorLocator: Locator, expectedText: string): Promise<void> {
60+
const content = await editorLocator.innerText();
61+
62+
if (!content || !content.includes(expectedText)) {
63+
throw new Error(`Expected text "${expectedText}" not found in editor content: "${content || ''}"`);
64+
}
65+
}
66+
67+
/**
68+
* Verifies that the Tiptap editor content equals the expected text.
69+
*
70+
* @param editorLocator - The Playwright locator for the editor element.
71+
* @param expectedText - The text to verify exactly matches the editor content.
72+
* @throws Error if the editor content does not exactly match the expected text.
73+
*/
74+
export async function equals(editorLocator: Locator, expectedText: string): Promise<void> {
75+
const content = await editorLocator.innerText();
76+
77+
if (content !== expectedText) {
78+
throw new Error(`Editor content does not match. Expected: "${expectedText}", Found: "${content || ''}"`);
79+
}
80+
}

tests/persistence.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ test("make sure that text is persistent between page refreshes", async ({
55
newtab,
66
}) => {
77
const testText = loremIpsum({ count: 1, units: "sentences" });
8-
await newtab.keyboard.type(testText);
8+
await newtab.keyboard.type(testText, {delay: 50});
99

10-
// refresh the page
10+
// refresh the page after 100ms
11+
await new Promise((resolve) => setTimeout(resolve, 100));
1112
await newtab.reload();
1213

1314
// wait for the text to sync

0 commit comments

Comments
 (0)