Skip to content
Draft
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