Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions e2e/tests/document-editing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { test, expect } from '@playwright/test';
import { openNewEditor, editorApi } from './helpers';

test.describe('Document editor - typing and formatting', () => {
test('type text and apply bold/italic', async ({ page }) => {
const { editorPage, frame } = await openNewEditor(page, 'a.try-editor.word', /\.docx/);

await editorPage.keyboard.type('Hello world');
await editorPage.keyboard.press('Control+A');
await editorPage.keyboard.press('Control+b');
await editorPage.keyboard.press('Control+i');

// The edit registered: the undo button is no longer disabled.
await expect(frame.locator('#slot-btn-undo button').first()).not.toHaveClass(/disabled/);

// The typed text round-trips through the automation API.
const text = await editorApi(editorPage, (api) => api.asc_GetSelectedText());
expect(text).toContain('Hello world');
});
});
87 changes: 87 additions & 0 deletions e2e/tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Page, expect } from '@playwright/test';

const EDITOR_IFRAME = 'iframe[name="frameEditor"]';

/**
* Open the example page, create a new document of the requested type, and wait
* until the editor iframe has finished loading. Returns the editor tab (a new
* page) and a frameLocator scoped to the editor iframe.
*/
export async function openNewEditor(page: Page, selector: string, urlExt: RegExp) {
await page.goto('/example/');
await expect(page).toHaveTitle(/ONLYOFFICE|euro-office/i);

const newPagePromise = page.context().waitForEvent('page');
await page.click(selector);
const editorPage = await newPagePromise;

await editorPage.waitForURL(/\/example\/editor/);
await editorPage.waitForLoadState('domcontentloaded');
await expect(editorPage).toHaveURL(urlExt);

const editorIframe = editorPage.locator(EDITOR_IFRAME);
await expect(editorIframe).toBeAttached({ timeout: 15_000 });

const frame = editorPage.frameLocator(EDITOR_IFRAME);
await expect(frame.locator('#loading-mask')).toBeHidden({ timeout: 30_000 });

await frame.locator('#editor_sdk').click();
return { editorPage, frame };
}

/**
* Evaluate a function against the editor's automation API (window.Asc.editor)
* inside the editor iframe. The function is serialized, so it must be
* self-contained (no closures over test scope).
*/
export async function editorApi<T>(editorPage: Page, fn: (api: any) => T): Promise<T> {
const handle = await editorPage.locator(EDITOR_IFRAME).elementHandle();
if (!handle) throw new Error('editor iframe not found');
const frame = await handle.contentFrame();
if (!frame) throw new Error('editor iframe has no content frame');

return frame.evaluate((body) => {
const w = window as unknown as { Asc?: { editor?: unknown } };
const api = w.Asc?.editor;
if (!api) throw new Error('window.Asc.editor not available');
// eslint-disable-next-line no-new-func
return new Function('api', `return (${body})(api);`)(api);
}, fn.toString());
}

/**
* Evaluate a function against the editor iframe's window, for driving app-level
* globals (e.g. Common.UI.Themes) rather than just the automation API. The
* function is serialized, so it must be self-contained (no closures over test
* scope) -- but real, JSON-serializable values from the test can still be
* passed in via `args`, which arrive as genuine extra parameters (`fn(win,
* ...args)`), not string-substituted into the function's source text.
*/
export async function frameEval<T>(
editorPage: Page,
fn: (win: any, ...args: any[]) => T,
args: any[] = [],
): Promise<T> {
const handle = await editorPage.locator(EDITOR_IFRAME).elementHandle();
if (!handle) throw new Error('editor iframe not found');
const frame = await handle.contentFrame();
if (!frame) throw new Error('editor iframe has no content frame');

return frame.evaluate(({ body, args }) => {
// eslint-disable-next-line no-new-func
return new Function('win', 'args', `return (${body}).apply(null, [win].concat(args));`)(window, args);
}, { body: fn.toString(), args });
}

/**
* Asserts an RGB color is within `tolerance` per channel of `expected`, not
* exactly equal. Anti-aliasing at a fill/glyph edge can shift a scanned
* pixel by a value or two from the exact color that was set, even when the
* color itself is correct.
*/
export function expectColorClose(actual: number[], expected: number[], tolerance = 4) {
expect(actual.length).toBe(expected.length);
for (let i = 0; i < expected.length; i++) {
expect(Math.abs(actual[i] - expected[i])).toBeLessThanOrEqual(tolerance);
}
}
24 changes: 24 additions & 0 deletions e2e/tests/presentation-slides.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from '@playwright/test';
import { openNewEditor, editorApi } from './helpers';

const slideCount = (p: import('@playwright/test').Page) =>
editorApi(p, (api) => api.getCountPages());

test.describe('Presentation editor - building a deck', () => {
test('create a deck of 3 slides', async ({ page }) => {
const { editorPage, frame } = await openNewEditor(page, 'a.try-editor.slide', /\.pptx/);

// A fresh presentation starts with a single slide.
expect(await slideCount(editorPage)).toBe(1);

// The Ctrl+M shortcut only fires when the slide-thumbnail panel is focused
// and toolbar button ids are renamed in this build, so drive slide creation
// through the automation API (AddSlide) instead.
await editorApi(editorPage, (api) => { api.AddSlide(); api.AddSlide(); });

await expect.poll(() => slideCount(editorPage)).toBe(3);

// The document was mutated: undo is enabled.
await expect(frame.locator('#slot-btn-undo button').first()).not.toHaveClass(/disabled/);
});
});
46 changes: 46 additions & 0 deletions e2e/tests/spreadsheet-formula.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test';
import { openNewEditor, editorApi } from './helpers';

const cellText = (editorPage: import('@playwright/test').Page) =>
editorApi(editorPage, (api) => {
const info = api.asc_getCellInfo();
return info && info.asc_getText ? info.asc_getText() : null;
});

test.describe('Spreadsheet editor - numbers and formulas', () => {
test('enter numbers and a formula', async ({ page }) => {
const { editorPage, frame } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/);
const nameBox = frame.locator('#ce-cell-name');
const select = async (cell: string) => {
await nameBox.fill(cell);
await nameBox.press('Enter');
};

// Ctrl+Home guarantees the grid has keyboard focus at A1 (the helper's
// canvas click can leave an arbitrary cell selected).
await editorPage.keyboard.press('Control+Home');
await editorPage.keyboard.type('5');
await editorPage.keyboard.press('Enter');
await editorPage.keyboard.type('10');
await editorPage.keyboard.press('Enter');
await editorPage.keyboard.type('=A1+A2');
await editorPage.keyboard.press('Enter');

// Operands landed in A1/A2.
await select('A1');
expect(await cellText(editorPage)).toBe('5');
await select('A2');
expect(await cellText(editorPage)).toBe('10');

// The formula was accepted into A3. asc_getText() returns the formula
// string, not the computed value.
await select('A3');
expect(await cellText(editorPage)).toBe('=A1+A2');

// TODO: verify the *computed* value (15). asc_getCellInfo().asc_getText()
// returns the formula, and pluginMethod_GetSelectedText() returns "" for
// cell selections, so neither reflects the result. The clipboard route
// (select A3 -> Ctrl+C -> navigator.clipboard.readText() === '15') works
// but needs clipboard-read/write permissions wired into the project config.
});
});
71 changes: 71 additions & 0 deletions e2e/tests/spreadsheeteditor/darkmode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
```
/$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$$ /$$
/$$__ $$ /$$__ $$| $$_____/ | $$__ $$ | $$
| $$ \__/| $$ \__/| $$ | $$ \ $$ /$$$$$$ /$$$$$$ | $$ /$$
| $$$$$$ | $$$$$$ | $$$$$ | $$ | $$ |____ $$ /$$__ $$| $$ /$$/
\____ $$ \____ $$| $$__/ | $$ | $$ /$$$$$$$| $$ \__/| $$$$$$/
/$$ \ $$ /$$ \ $$| $$ | $$ | $$ /$$__ $$| $$ | $$_ $$
| $$$$$$/| $$$$$$/| $$$$$$$$ | $$$$$$$/| $$$$$$$| $$ | $$ \ $$
\______/ \______/ |________/ |_______/ \_______/|__/ |__/ \__/
/$$ /$$ /$$ /$$$$$$$$ /$$
| $$$ /$$$ | $$ |__ $$__/ | $$
| $$$$ /$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ /$$$$$$ /$$$$$$$ /$$$$$$
| $$ $$/$$ $$ /$$__ $$ /$$__ $$ /$$__ $$ | $$ /$$__ $$ /$$_____/|_ $$_/
| $$ $$$| $$| $$ \ $$| $$ | $$| $$$$$$$$ | $$| $$$$$$$$| $$$$$$ | $$
| $$\ $ | $$| $$ | $$| $$ | $$| $$_____/ | $$| $$_____/ \____ $$ | $$ /$$
| $$ \/ | $$| $$$$$$/| $$$$$$$| $$$$$$$ | $$| $$$$$$$ /$$$$$$$/ | $$$$/
|__/ |__/ \______/ \_______/ \_______/ |__/ \_______/|_______/ \___/
```

## what we want to test

UI and relation with darkmode
- button darkmode being disabled in light theme
- button darkmode being enabled in dark theme
- click on button darkmode enable darkmode
- interface theme's effect on content dark mode (forces off / restores)

background and foreground colors cells
- 100% automatic colors , before/after darkmode
- automatic background color + user text color , before/after darkmode
- user background color + automatic text color , before/after darkmode
- user background color + user text color , before/after darkmode
- merged cells with automatic colors
- search-highlighted cell text contrast
- a cell actively being edited (mid-edit toggle)

borders
- 100% automatic colors , before/after darkmode
- user/explicit color , before/after darkmode
- column resize guide visibility

print (simplified test)
- backgroundcolor of document , before/after darkmode

## running these tests

Each file in this folder is a normal Playwright spec, discovered directly --
no index, no special setup.

```bash
# all of them
npx playwright test tests/spreadsheeteditor/darkmode/

# just one
npx playwright test tests/spreadsheeteditor/darkmode/resize-guide.spec.ts

# by name
npx playwright test tests/spreadsheeteditor/darkmode/ -g "resize guide"

# everything except one
npx playwright test tests/spreadsheeteditor/darkmode/ --grep-invert "resize guide"
```

If a test fails with `page.goto` timing out or a `test.describe()`-related error
that doesn't match the actual file content, clear Playwright's transform cache
before assuming it's a real bug -- this has recurred a few times after
renaming/moving spec files in this folder:

```bash
rm -rf /tmp/playwright-transform-cache-*
```
57 changes: 57 additions & 0 deletions e2e/tests/spreadsheeteditor/darkmode/cell-borders.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { test, expect } from '@playwright/test';
import { openNewEditor, editorApi, frameEval, expectColorClose } from '../../helpers';
import { bottomBorderPixel } from '../../utils/spreadsheet-editor';

test.describe('Spreadsheet editor - dark mode rendering', () => {
/*
TESTING CELL BORDER COLOR CORRECTION
*/
test('automatic cell border color inverts in dark mode; explicit border color stays untouched', async ({ page }) => {
// C2 gets an automatic-colored bottom border, D2 an explicit one.
const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/);

// asc_CBorder(style, color) -- omitting color leaves it automatic (null).
await editorApi(editorPage, (api) => {
api.asc_findCell('C2');
const borders: any[] = [];
borders[(window as any).Asc.c_oAscBorderOptions.Bottom] = new (window as any).Asc.asc_CBorder((window as any).Asc.c_oAscBorderStyles.Thin);
api.asc_setCellBorders(borders);
});
await editorApi(editorPage, (api) => {
api.asc_findCell('D2');
const borders: any[] = [];
borders[(window as any).Asc.c_oAscBorderOptions.Bottom] =
new (window as any).Asc.asc_CBorder((window as any).Asc.c_oAscBorderStyles.Thin, new (window as any).Asc.asc_CColor(220, 20, 20));
api.asc_setCellBorders(borders);
});

// asc_setCellBorders lands on the model a tick later -- poll rather than
// assume it's synchronous. Uses frameEval (not editorApi) since row/col
// need to travel as real args.
const bottomBorderWidth = (row: number, col: number) =>
frameEval(editorPage, (win, row: number, col: number) => {
const b = win.Asc.editor.wb.getWorksheet().model.getRange3(row, col, row, col).getBorderFull();
return b.b ? b.b.w : 0;
}, [row, col]);
await expect.poll(() => bottomBorderWidth(1, 2)).toBeGreaterThan(0);
await expect.poll(() => bottomBorderWidth(1, 3)).toBeGreaterThan(0);

// MEASURE BORDER COLOR IN LIGHT MODE

// C2's automatic border resolves to literal black by default, same as
// automatic text.
expectColorClose(await bottomBorderPixel(editorPage, 1, 2), [0, 0, 0]);
expectColorClose(await bottomBorderPixel(editorPage, 1, 3), [220, 20, 20]);

// SWITCH TO DARK MODE

await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true));

// MEASURE BORDER COLOR IN DARK MODE

// C2's automatic border inverts for contrast; D2's explicit border is
// exactly unchanged.
await expect.poll(() => bottomBorderPixel(editorPage, 1, 2)).toEqual([255, 255, 255]);
expectColorClose(await bottomBorderPixel(editorPage, 1, 3), [220, 20, 20]);
});
});
Loading
Loading