Skip to content

Commit

Permalink
Merge pull request #124 from cpinitiative/codemirror
Browse files Browse the repository at this point in the history
Add mobile support
  • Loading branch information
thecodingwizard authored Dec 20, 2023
2 parents d93fbc1 + 6e3dfd0 commit 1fb41f5
Show file tree
Hide file tree
Showing 32 changed files with 817 additions and 145 deletions.
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# used for copying files
# e2e tests use this
YJS_SECURITY_KEY=
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ yarn-error.log*
/test-results/
/playwright-report/
/playwright/.cache/
# private folder where I store firebase admin key
private
.env
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ You can update the Firebase configuration (if you want to use a custom firebase

- Code execution through a custom [Serverless Online Judge](https://github.com/cpinitiative/online-judge)
- Realtime collaboration with [YJS](https://github.com/yjs/yjs)
- Monaco Editor
- Monaco Editor (desktop)
- Codemirror 6 Editor (mobile)
- [monaco-languageclient](https://github.com/TypeFox/monaco-languageclient) with `clangd-12` for LSP
- React
- Jotai
Expand Down
29 changes: 13 additions & 16 deletions e2e/copies_files.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import { test, expect, Page } from '@playwright/test';
import { host } from './helpers';
import { test, expect } from '@playwright/test';
import {
host,
setInputEditorValue,
setMainEditorValue,
waitForEditorToLoad,
} from './helpers';

test.describe('Basic Functionality', () => {
test('should copy files', async ({ page, context }) => {
test('should copy files', async ({ page }) => {
await page.goto(`${host}/n`);
await page.waitForSelector('button:has-text("Run Code")');

// let monaco load
await page.waitForTimeout(500);
await waitForEditorToLoad(page);

await page.click('[data-test-id="input-editor"]');
await page.keyboard.type('1 2 3');

await page.evaluate(
`this.monaco.editor.getModels().find(x => x.getLanguageId() === "cpp").setValue(\`code_value\`)`
);
await page.evaluate(
`this.monaco.editor.getModels().find(x => x.getLanguageId() === "plaintext").setValue(\`input_value\`)`
);
await setMainEditorValue(page, 'code_value', 'cpp');
await setInputEditorValue(page, 'input_value');

// sync with yjs server
await page.waitForTimeout(1500);

await page.goto(page.url() + '/copy');
await page.waitForSelector('button:has-text("Run Code")');

expect(await page.$('text="code_value"')).toBeTruthy();
expect(await page.$('text="input_value"')).toBeTruthy();
await expect(page.getByText('code_value')).toHaveCount(1);
await expect(page.getByText('input_value')).toHaveCount(1);
});
});
16 changes: 12 additions & 4 deletions e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { test, expect, Page } from '@playwright/test';
import { host } from './helpers';

test.describe('Dashboard Page', () => {
test('should show recently accessed files', async ({ page, context }) => {
test('should show recently accessed files', async ({ page, isMobile }) => {
await page.goto(`${host}`);
await page.locator('text=Loading...').waitFor({ state: 'hidden' });
expect(await page.$('text=Unnamed Workspace')).toBeFalsy();

await page.goto(`${host}/n`);
await page.waitForSelector('button:has-text("Run Code")');
await page.waitForSelector('button:has-text("1 User Online")');
if (!isMobile) {
await page.waitForSelector('button:has-text("1 User Online")');
}
expect(page.url()).toMatch(new RegExp(`${host}/[A-z0-9_-]{19}`));
await page.waitForTimeout(1500); // waiting for file info to be uploaded to firebase

Expand All @@ -22,7 +24,11 @@ test.describe('Dashboard Page', () => {
expect(await page.$('text="Me"')).toBeTruthy();
});

test('should show owner field properly', async ({ page, browser }) => {
test('should show owner field properly', async ({
page,
browser,
isMobile,
}) => {
await page.goto(`${host}/n`);
await page.waitForSelector('button:has-text("Run Code")');
await page.locator('text=File').click();
Expand All @@ -38,7 +44,9 @@ test.describe('Dashboard Page', () => {
const page2 = await context2.newPage();
await page2.goto(page.url());
await page2.waitForSelector('button:has-text("Run Code")');
await page2.waitForSelector('button:has-text("2 Users Online")');
if (!isMobile) {
await page2.waitForSelector('button:has-text("2 Users Online")');
}
expect(page2.url()).toMatch(new RegExp(`${host}/[A-z0-9_-]{19}`));
await page2.waitForTimeout(1500); // waiting for file info to be uploaded to firebase

Expand Down
70 changes: 65 additions & 5 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,25 @@ export async function goToPage(page1: Page, page2: Page) {
// );
}

export const testRunCode = async (page: Page): Promise<void> => {
export const testRunCode = async (
page: Page,
isMobile: boolean
): Promise<void> => {
if (isMobile) {
await page.click('text=Input/Output');
}
await page.locator('button:has-text("stdout")').click();
await page.click('button:has-text("Run Code")');
expect(await page.$('[data-test-id="run-code-loading"]')).toBeTruthy();
await page.waitForSelector('button:has-text("Run Code")');
expect(await page.$('text=Successful')).toBeTruthy();
await expect(page.getByText('Successful')).toBeVisible({ timeout: 1000 });
await page.locator('button:has-text("stdout")').click();
expect(
await page.$('text="The sum of these three numbers is 6"')
).toBeTruthy();
await expect(
page.getByText('The sum of these three numbers is 6')
).toBeVisible({ timeout: 1000 });
if (isMobile) {
await page.getByTestId('mobile-bottom-nav-code-button').click();
}
};

export const switchLang = async (
Expand All @@ -55,3 +64,54 @@ export const forEachLang = async (
await switchLang(page, 'C++');
await func();
};

export const waitForEditorToLoad = async (page: Page): Promise<void> => {
// wait for monaco / codemirror to load (monaco / codemirror is a lazy component, so it may take some time for it to load)
await expect(page.getByTestId('editorLoadingMessage')).toHaveCount(0);
};

export const isMonaco = async (page: Page): Promise<boolean> => {
// note: this is a hacky way to detect whether we are using monaco.
// in particular, it's possible for this.monaco to be loaded,
// for us to resize the window to be small, and for us to use codemirror
// even though this.monaco is set
if (await page.evaluate(`this.monaco`)) {
return true;
}
return false;
};

export const setMainEditorValue = async (
page: Page,
value: string,
language: string // needed for monaco only
): Promise<void> => {
if (await isMonaco(page)) {
await page.evaluate(
`this.monaco.editor.getModels().find(x => x.getLanguageId() === "${language}").setValue(\`${value}\`)`
);
} else {
await page.evaluate(`
this.TEST_mainCodemirrorEditor.dispatch({
changes: {from: 0, to: this.TEST_mainCodemirrorEditor.state.doc.length, insert: \`${value}\`}
});
`);
}
};

export const setInputEditorValue = async (
page: Page,
value: string
): Promise<void> => {
if (await isMonaco(page)) {
await page.evaluate(
`this.monaco.editor.getModels().find(x => x.getLanguageId() === "plaintext").setValue(\`${value}\`)`
);
} else {
await page.evaluate(`
this.TEST_inputCodemirrorEditor.dispatch({
changes: {from: 0, to: this.TEST_inputCodemirrorEditor.state.doc.length, insert: \`${value}\`}
});
`);
}
};
19 changes: 19 additions & 0 deletions e2e/mobile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test, expect } from '@playwright/test';
import { host, isMonaco, waitForEditorToLoad } from './helpers';

test.describe('Mobile Specific Checks', () => {
test('should load monaco for desktop and codemirror for mobile', async ({
page,
isMobile,
}) => {
await page.goto(`${host}/n`);
await page.waitForSelector('button:has-text("Run Code")');
await waitForEditorToLoad(page);

if (isMobile) {
expect(await isMonaco(page)).toBeFalsy();
} else {
expect(await isMonaco(page)).toBeTruthy();
}
});
});
77 changes: 54 additions & 23 deletions e2e/respects_permissions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { test, expect, Page } from '@playwright/test';
import { testRunCode, goToPage, createNew, switchLang } from './helpers';
import {
testRunCode,
goToPage,
createNew,
switchLang,
isMonaco,
setInputEditorValue,
waitForEditorToLoad,
} from './helpers';

// note: these tests are currently quite bad -- we need error handling for when permission is denied
// rather than just silently failing.

test.describe('Respects Permissions', () => {
test('should support view only', async ({ page, browser }) => {
test('should support view only', async ({ page, browser, isMobile }) => {
const context2 = await browser.newContext();

const page2 = await context2.newPage();
Expand All @@ -21,22 +29,28 @@ test.describe('Respects Permissions', () => {
await page2.waitForSelector('text="View Only"');

// let monaco load
await page.waitForTimeout(500);
await page2.waitForTimeout(500);
await waitForEditorToLoad(page);
await waitForEditorToLoad(page2);

if (isMobile) {
await page.click('text=Input/Output');
await page2.click('text=Input/Output');
}

// test input
await page2.click('[data-test-id="input-editor"]');
await page2.waitForTimeout(200);
await page2.keyboard.type('1 2 3');
await page2.keyboard.type(' 4 5 6');
await page2.waitForTimeout(200);
expect(await page2.$('text="1 2 3"')).toBeFalsy();
expect(await page2.$('text="1 2 3 4 5 6"')).toBeFalsy();

await page.click('[data-test-id="input-editor"]');
await page.waitForTimeout(200);
await page.keyboard.type('1 2 3');
await page.waitForTimeout(200);
expect(await page.$('text="1 2 3"')).toBeTruthy();
await page2.waitForSelector('text="1 2 3"');
await page.keyboard.type(' 4 5 6');
// test only 4 5 6 here because sometimes the cursor is in between the 3 and the 4
// and the cursor in codemirror has some text ://
await expect(page.getByText('4 5 6')).toBeVisible();
await expect(page2.getByText('4 5 6')).toBeVisible();

// test scribble
await page2.click('text=scribble');
Expand All @@ -56,34 +70,42 @@ test.describe('Respects Permissions', () => {
expect(await page.$('text="testing scribble"')).toBeTruthy();
await page2.waitForSelector('text="testing scribble"');

if (isMobile) {
await page.getByTestId('mobile-bottom-nav-code-button').click();
await page2.getByTestId('mobile-bottom-nav-code-button').click();
}

// the class of the div containing the editor is different for monaco and codemirror
const editorClass = (await isMonaco(page)) ? '.view-lines' : '.cm-content';

// test editor
await page2.click('.view-lines div:nth-child(10)');
await page2.click(`${editorClass} div:nth-child(10)`);
await page.waitForTimeout(200);
await page2.keyboard.type('// this is a comment');
await page.waitForTimeout(200);
expect(await page2.$('text="// this is a comment"')).toBeFalsy();
await page.click('.view-lines div:nth-child(10)');
await page.click(`${editorClass} div:nth-child(10)`);
await page.waitForTimeout(200);
await page.keyboard.type('// this is a comment');
await page.waitForTimeout(200);
expect(await page.$('text="// this is a comment"')).toBeTruthy();
await page2.waitForSelector('text="// this is a comment"');

// test run buttons -- only the first page should work
await testRunCode(page);
await testRunCode(page, isMobile);
await expect(
page2.getByRole('button', { name: 'Run Code' })
).toBeDisabled();

await switchLang(page, 'Java');
await page.waitForSelector('button:has-text("Run Code")');
await page2.waitForSelector('button:has-text("Run Code")');
await page2.click('.view-lines div:nth-child(2)');
await page2.click(`${editorClass} div:nth-child(2)`);
await page.waitForTimeout(200);
await page2.keyboard.type('// this is a comment');
await page.waitForTimeout(200);
expect(await page2.$('text="// this is a comment"')).toBeFalsy();
await page.click('.view-lines div:nth-child(2)');
await page.click(`${editorClass} div:nth-child(2)`);
await page.waitForTimeout(200);
await page.keyboard.type('// this is a comment');
await page.waitForTimeout(200);
Expand All @@ -93,12 +115,12 @@ test.describe('Respects Permissions', () => {
await switchLang(page, 'Python 3.8.1');
await page.waitForSelector('button:has-text("Run Code")');
await page2.waitForSelector('button:has-text("Run Code")');
await page2.click('.view-lines div:nth-child(5)');
await page2.click(`${editorClass} div:nth-child(5)`);
await page.waitForTimeout(200);
await page2.keyboard.type('# this is a comment');
await page.waitForTimeout(200);
expect(await page2.$('text="# this is a comment"')).toBeFalsy();
await page.click('.view-lines div:nth-child(5)');
await page.click(`${editorClass} div:nth-child(5)`);
await page.waitForTimeout(200);
await page.keyboard.type('# this is a comment');
await page.waitForTimeout(200);
Expand All @@ -112,6 +134,7 @@ test.describe('Respects Permissions', () => {
test('should work when default permission is changed', async ({
page,
browser,
isMobile,
}) => {
const context2 = await browser.newContext();

Expand All @@ -124,13 +147,21 @@ test.describe('Respects Permissions', () => {
await page2.waitForSelector('button:has-text("Run Code")');

// let monaco load
await waitForEditorToLoad(page);
await waitForEditorToLoad(page2);
// unclear if these are needed -- maybe yjs needs time to initialize.
await page.waitForTimeout(500);
await page2.waitForTimeout(500);

if (isMobile) {
await page.click('text=Input/Output');
await page2.click('text=Input/Output');
}

// test input: everything should still work right now
await page2.click('[data-test-id="input-editor"]');
await page2.keyboard.type('1 2 3');
await page2.waitForSelector('text="1 2 3"');
await page2.keyboard.type(' 4');
await page2.waitForSelector('text="1 2 3 4"');

// try view only
await page.click('text=File');
Expand All @@ -141,8 +172,8 @@ test.describe('Respects Permissions', () => {

// test input: we shouldn't be able to type anything on page 2
await page2.click('[data-test-id="input-editor"]');
await page2.keyboard.type('4 5 6');
expect(await page2.$('text="4 5 6"')).toBeFalsy();
await page2.keyboard.type(' 0');
expect(await page2.$('text="1 2 3 4 5 6 0"')).toBeFalsy();

// try private
await page.click('text=File');
Expand All @@ -162,9 +193,9 @@ test.describe('Respects Permissions', () => {

// test input: we should be able to type stuff now
await page2.click('[data-test-id="input-editor"]');
await page2.keyboard.type('0 9 8');
await page2.keyboard.type(' 1');
// 1 2 3 from above
await page2.waitForSelector('text="1 2 30 9 8"');
await page2.waitForSelector('text="1 2 3 4 1"');

await page2.close();
await context2.close();
Expand Down
Loading

1 comment on commit 1fb41f5

@vercel
Copy link

@vercel vercel bot commented on 1fb41f5 Dec 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.