From 16c85969fb6b0af38baa00dc59dd36652972a3ad Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Tue, 16 Jun 2026 22:23:42 +0200 Subject: [PATCH 1/8] test(e2e): add editor smoke tests for typing, formulas and slides Add Playwright smoke tests covering basic editing in each editor: - document: type text, apply bold/italic, verify text round-trips - spreadsheet: enter numbers and a formula, verify it is accepted - presentation: build a three-slide deck Shared helpers open a new document from the example app and reach the editor automation API inside the iframe. Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Julius Knorr --- e2e/tests/document-editing.spec.ts | 20 +++++++++++ e2e/tests/helpers.ts | 50 +++++++++++++++++++++++++++ e2e/tests/presentation-slides.spec.ts | 24 +++++++++++++ e2e/tests/spreadsheet-formula.spec.ts | 46 ++++++++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 e2e/tests/document-editing.spec.ts create mode 100644 e2e/tests/helpers.ts create mode 100644 e2e/tests/presentation-slides.spec.ts create mode 100644 e2e/tests/spreadsheet-formula.spec.ts diff --git a/e2e/tests/document-editing.spec.ts b/e2e/tests/document-editing.spec.ts new file mode 100644 index 0000000000..a552b6c925 --- /dev/null +++ b/e2e/tests/document-editing.spec.ts @@ -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'); + }); +}); diff --git a/e2e/tests/helpers.ts b/e2e/tests/helpers.ts new file mode 100644 index 0000000000..e1a69a0c75 --- /dev/null +++ b/e2e/tests/helpers.ts @@ -0,0 +1,50 @@ +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(editorPage: Page, fn: (api: any) => T): Promise { + 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()); +} diff --git a/e2e/tests/presentation-slides.spec.ts b/e2e/tests/presentation-slides.spec.ts new file mode 100644 index 0000000000..f2a6d2aa00 --- /dev/null +++ b/e2e/tests/presentation-slides.spec.ts @@ -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/); + }); +}); diff --git a/e2e/tests/spreadsheet-formula.spec.ts b/e2e/tests/spreadsheet-formula.spec.ts new file mode 100644 index 0000000000..e56b6492e8 --- /dev/null +++ b/e2e/tests/spreadsheet-formula.spec.ts @@ -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. + }); +}); From 7108e0e2d907bc479662979f071cddf08a111280 Mon Sep 17 00:00:00 2001 From: j-base64 Date: Mon, 27 Jul 2026 20:36:06 +0200 Subject: [PATCH 2/8] test(e2e): add spreadsheet dark-mode coverage Covers the toggle button, interface-theme/content-dark-mode interaction, cell color/border/merge/search-highlight/resize-guide rendering, mid-edit toggle behavior, and print-preview isolation from content dark mode. One test per file under e2e/tests/spreadsheeteditor/darkmode/, registered via the spreadsheeteditor-darkmode.spec.ts index (run with --workers=1 to avoid overloading the shared dev container). helpers.ts gains frameEval arg support and a shared expectColorClose assertion. Signed-off-by: j-base64 Assisted-by: ClaudeCode:claude-sonnet-5 --- e2e/tests/helpers.ts | 37 +++++ e2e/tests/spreadsheeteditor-darkmode.spec.ts | 76 +++++++++ .../darkmode/cell-borders.ts | 57 +++++++ .../spreadsheeteditor/darkmode/cell-colors.ts | 113 +++++++++++++ .../darkmode/interface-theme.ts | 29 ++++ .../darkmode/merged-cells.ts | 48 ++++++ .../darkmode/mid-edit-toggle.ts | 48 ++++++ .../darkmode/print-preview.ts | 54 ++++++ .../darkmode/resize-guide.ts | 34 ++++ .../darkmode/search-highlight.ts | 65 ++++++++ .../darkmode/toggle-button.ts | 45 +++++ e2e/tests/utils/spreadsheet-editor.ts | 156 ++++++++++++++++++ 12 files changed, 762 insertions(+) create mode 100644 e2e/tests/spreadsheeteditor-darkmode.spec.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/cell-borders.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/cell-colors.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/interface-theme.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/merged-cells.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/print-preview.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/resize-guide.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/search-highlight.ts create mode 100644 e2e/tests/spreadsheeteditor/darkmode/toggle-button.ts create mode 100644 e2e/tests/utils/spreadsheet-editor.ts diff --git a/e2e/tests/helpers.ts b/e2e/tests/helpers.ts index e1a69a0c75..e799f207cb 100644 --- a/e2e/tests/helpers.ts +++ b/e2e/tests/helpers.ts @@ -48,3 +48,40 @@ export async function editorApi(editorPage: Page, fn: (api: any) => T): Promi 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( + editorPage: Page, + fn: (win: any, ...args: any[]) => T, + args: any[] = [], +): Promise { + 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); + } +} diff --git a/e2e/tests/spreadsheeteditor-darkmode.spec.ts b/e2e/tests/spreadsheeteditor-darkmode.spec.ts new file mode 100644 index 0000000000..2ae97a27f0 --- /dev/null +++ b/e2e/tests/spreadsheeteditor-darkmode.spec.ts @@ -0,0 +1,76 @@ +/* + + + /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$$ /$$ + /$$__ $$ /$$__ $$| $$_____/ | $$__ $$ | $$ +| $$ \__/| $$ \__/| $$ | $$ \ $$ /$$$$$$ /$$$$$$ | $$ /$$ +| $$$$$$ | $$$$$$ | $$$$$ | $$ | $$ |____ $$ /$$__ $$| $$ /$$/ + \____ $$ \____ $$| $$__/ | $$ | $$ /$$$$$$$| $$ \__/| $$$$$$/ + /$$ \ $$ /$$ \ $$| $$ | $$ | $$ /$$__ $$| $$ | $$_ $$ +| $$$$$$/| $$$$$$/| $$$$$$$$ | $$$$$$$/| $$$$$$$| $$ | $$ \ $$ + \______/ \______/ |________/ |_______/ \_______/|__/ |__/ \__/ + /$$ /$$ /$$ /$$$$$$$$ /$$ +| $$$ /$$$ | $$ |__ $$__/ | $$ +| $$$$ /$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ /$$$$$$ /$$$$$$$ /$$$$$$ +| $$ $$/$$ $$ /$$__ $$ /$$__ $$ /$$__ $$ | $$ /$$__ $$ /$$_____/|_ $$_/ +| $$ $$$| $$| $$ \ $$| $$ | $$| $$$$$$$$ | $$| $$$$$$$$| $$$$$$ | $$ +| $$\ $ | $$| $$ | $$| $$ | $$| $$_____/ | $$| $$_____/ \____ $$ | $$ /$$ +| $$ \/ | $$| $$$$$$/| $$$$$$$| $$$$$$$ | $$| $$$$$$$ /$$$$$$$/ | $$$$/ +|__/ |__/ \______/ \_______/ \_______/ |__/ \_______/|_______/ \___/ + + + +what we want to test + +UI and relation with darkmode +- button darkmode beeing dsiabled in light theme +- button darkmode beeing enabled in dark theme +- click on button darkmode enable darkmode + +background and foreground colors cells +- 100% automatic colors , before/after darkmode +- automatic background color + usertextcolor , before/after darkmode +- user background color + automatic text color , before/after darkmode +- user background color + user text color , before/after darkmode + +borders +- 100% automatic colors , before/after darkmode +- + +print +- backgroundcolor of document , before/after darkmode + + +*/ + + + + + +// Index for the spreadsheet dark-mode suite -- imports each test file under +// spreadsheeteditor/darkmode/ for its side effect (registering its test() +// calls). Those files use a plain .ts extension (not .spec.ts/.test.ts) so +// Playwright's own file discovery doesn't also pick them up directly and +// run them a second time. +// +// Run this suite with --workers=1, e.g.: +// npx playwright test tests/spreadsheeteditor-darkmode.spec.ts --workers=1 +// Splitting into one file per test lets Playwright schedule up to ~7 +// workers in parallel by default on this machine (cpus/2). Confirmed live: +// running 3 grouped files already used 3 workers and produced page.goto +// navigation timeouts against the shared dev container under that +// concurrent load -- 9 separate files would make more workers available, +// not fewer. Forcing one worker keeps these tests running against the +// shared container one at a time, like the rest of this suite already +// does when run as a single file. + +import './spreadsheeteditor/darkmode/toggle-button'; +import './spreadsheeteditor/darkmode/interface-theme'; +import './spreadsheeteditor/darkmode/cell-colors'; +import './spreadsheeteditor/darkmode/cell-borders'; +import './spreadsheeteditor/darkmode/merged-cells'; +import './spreadsheeteditor/darkmode/search-highlight'; +import './spreadsheeteditor/darkmode/resize-guide'; +import './spreadsheeteditor/darkmode/mid-edit-toggle'; +import './spreadsheeteditor/darkmode/print-preview'; + diff --git a/e2e/tests/spreadsheeteditor/darkmode/cell-borders.ts b/e2e/tests/spreadsheeteditor/darkmode/cell-borders.ts new file mode 100644 index 0000000000..a8280fba9f --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/cell-borders.ts @@ -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]); + }); +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/cell-colors.ts b/e2e/tests/spreadsheeteditor/darkmode/cell-colors.ts new file mode 100644 index 0000000000..bcea530467 --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/cell-colors.ts @@ -0,0 +1,113 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, expectColorClose } from '../../helpers'; +import { cellFillRgb, cellFontRgb, sampleCellPixels } from '../../utils/spreadsheet-editor'; + +test.describe('Spreadsheet editor - dark mode rendering', () => { + /* + TESTING CORE CELL COLOR CORRECTION + */ + test('automatic cell colors invert in dark mode; explicit colors stay untouched', async ({ page }) => { + // Covers the four automatic/explicit combinations a cell's fill and text + // can be in: B2 (both automatic), B3 (fill only), B4 (both explicit), + // B6 (text only). Measures each before/after toggling dark mode. + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + // Assert the light-mode starting point explicitly rather than assume it + // -- every "before" check below relies on it. + expect(await editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); + + // WRITE B2/B3/B4/B6 + + // B2: left on default/automatic colors -- no fill, no explicit text color. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(1, 1, 1, 1).setValue('lorem'); + }); + + // B3: explicit fill only. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(2, 1, 2, 1).setValue('bgOnly'); + }); + + // asc_setCellBackgroundColor/asc_setCellTextColor land on the model a + // tick after the call returns -- poll rather than assume it's synchronous. + await editorApi(editorPage, (api) => { + api.asc_findCell('B3'); + api.asc_setCellBackgroundColor(new (window as any).Asc.asc_CColor(200, 100, 50)); + }); + await expect.poll(() => cellFillRgb(editorPage, 2, 1)).not.toBeNull(); + + // B4: explicit fill + explicit text color. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(3, 1, 3, 1).setValue('bgAndText'); + api.asc_findCell('B4'); + api.asc_setCellBackgroundColor(new (window as any).Asc.asc_CColor(30, 30, 120)); + }); + await expect.poll(() => cellFillRgb(editorPage, 3, 1)).not.toBeNull(); + + await editorApi(editorPage, (api) => { + api.asc_findCell('B4'); + api.asc_setCellTextColor(new (window as any).Asc.asc_CColor(255, 220, 0)); + }); + await expect.poll(() => cellFontRgb(editorPage, 3, 1)).not.toBeNull(); + + // B6: automatic fill (none) + explicit text color only. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(5, 1, 5, 1).setValue('textOnly'); + api.asc_findCell('B6'); + api.asc_setCellTextColor(new (window as any).Asc.asc_CColor(10, 10, 60)); + }); + await expect.poll(() => cellFontRgb(editorPage, 5, 1)).not.toBeNull(); + + // MEASURE COLORS IN LIGHT MODE + + const b2Before = await sampleCellPixels(editorPage, 1, 1); + const b3Before = await sampleCellPixels(editorPage, 2, 1); + const b4Before = await sampleCellPixels(editorPage, 3, 1); + const b6Before = await sampleCellPixels(editorPage, 5, 1); + + // B2's automatic text is literal black against the light-mode canvas. + expectColorClose(b2Before.darkest, [0, 0, 0]); + // B3's explicit fill renders as set. + expectColorClose(b3Before.lightest, [200, 100, 50]); + // B4's explicit fill and explicit text both render as set. + expectColorClose(b4Before.darkest, [30, 30, 120]); + expectColorClose(b4Before.lightest, [255, 220, 0]); + // B6's explicit text renders as set; its automatic (no-fill) background + // is plain white in light mode. + expectColorClose(b6Before.darkest, [10, 10, 60]); + expectColorClose(b6Before.lightest, [255, 255, 255]); + + // SWITCH TO DARK MODE + + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + + // MEASURE COLORS IN DARK MODE + + // Poll rather than sample immediately after the toggle -- draw() may not + // land within the same tick asc_setContentDarkMode returns in. + await expect.poll(async () => (await sampleCellPixels(editorPage, 1, 1)).lightest).toEqual([255, 255, 255]); + + const b3After = await sampleCellPixels(editorPage, 2, 1); + const b4After = await sampleCellPixels(editorPage, 3, 1); + const b6After = await sampleCellPixels(editorPage, 5, 1); + + // Explicit fill/text colors are exactly unchanged by the dark-mode toggle. + expectColorClose(b3After.lightest, [200, 100, 50]); + expectColorClose(b4After.darkest, [30, 30, 120]); + expectColorClose(b4After.lightest, [255, 220, 0]); + // KNOWN BUG: B3's automatic text should invert for contrast in dark mode + // the same way B2's does, but any explicit fill -- light or dark -- + // currently forces it to stay literal black instead. This asserts + // today's actual (wrong) output on purpose: it starts failing the + // moment that bug is fixed, which is the signal to update it, not a + // regression to chase. + expectColorClose(b3After.darkest, [0, 0, 0]); + // B6's explicit text is still unchanged; its automatic background is now + // the dark canvas gray instead of white -- the two swap which slot + // (darkest/lightest) they land in, since the text color (sum 80) is + // darker than both the light-mode white background (sum 765) and the + // dark-mode gray background (sum 114), so it's "darkest" in both modes. + expectColorClose(b6After.darkest, [10, 10, 60]); + expectColorClose(b6After.lightest, [38, 38, 38]); + }); +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/interface-theme.ts b/e2e/tests/spreadsheeteditor/darkmode/interface-theme.ts new file mode 100644 index 0000000000..f0bac47f10 --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/interface-theme.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, frameEval } from '../../helpers'; + +test.describe('Spreadsheet editor - dark mode', () => { + /* + TESTING INTERFACE THEME'S EFFECT ON CONTENT DARK MODE + */ + test('interface theme forces content dark mode off, then restores it -- not fully independent', async ({ page }) => { + // Switching interface theme to light unconditionally forces content + // dark mode off; switching back to dark restores the remembered + // preference automatically. So it's "remembered, but suspended while + // the interface is light," not fully independent of interface theme. + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + await frameEval(editorPage, (win) => win.Common.UI.Themes.setTheme('theme-night')); + await frameEval(editorPage, (win) => win.Common.UI.Themes.setContentTheme('dark')); + await expect.poll(() => editorApi(editorPage, (api) => api.isDarkMode)).toBe(true); + + // SWITCH INTERFACE THEME TO LIGHT -- CONTENT DARK MODE FORCED OFF + + await frameEval(editorPage, (win) => win.Common.UI.Themes.setTheme('theme-classic-light')); + await expect.poll(() => editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); + + // SWITCH INTERFACE THEME BACK TO DARK -- PREFERENCE IS RESTORED + + await frameEval(editorPage, (win) => win.Common.UI.Themes.setTheme('theme-night')); + await expect.poll(() => editorApi(editorPage, (api) => api.isDarkMode)).toBe(true); + }); +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/merged-cells.ts b/e2e/tests/spreadsheeteditor/darkmode/merged-cells.ts new file mode 100644 index 0000000000..d4b8768a60 --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/merged-cells.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, expectColorClose } from '../../helpers'; +import { sampleMergedCellPixels } from '../../utils/spreadsheet-editor'; + +test.describe('Spreadsheet editor - dark mode rendering', () => { + /* + TESTING MERGED CELL COLOR CORRECTION + */ + test('merged cell with automatic colors inverts the same way a regular cell does', async ({ page }) => { + // A merged cell must follow the same automatic/explicit color rules as + // a regular cell, not some mismatched combination of the two. + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + // B2:C2 merged, left on automatic colors (no fill, no explicit text). + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(1, 1, 1, 1).setValue('merged text'); + api.asc_findCell('B2:C2'); + }); + await editorApi(editorPage, (api) => api.asc_mergeCells((window as any).Asc.c_oAscMergeOptions.Merge)); + + // asc_mergeCells lands on the model a tick later -- poll rather than + // assume it's synchronous. + await expect.poll(() => + editorApi(editorPage, (api) => { + const merged = api.wb.getWorksheet().model.getMergedByCell(1, 1); + return merged ? [merged.r1, merged.c1, merged.r2, merged.c2] : null; + }), + ).toEqual([1, 1, 1, 2]); + + // MEASURE COLORS IN LIGHT MODE + + const before = await sampleMergedCellPixels(editorPage, 1, 1, 2); + expectColorClose(before.darkest, [0, 0, 0]); + expectColorClose(before.lightest, [255, 255, 255]); + + // SWITCH TO DARK MODE + + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + + // MEASURE COLORS IN DARK MODE + + // Text inverts to white, background inverts to the dark canvas gray -- + // the same pattern a regular automatic cell follows. + await expect.poll(async () => (await sampleMergedCellPixels(editorPage, 1, 1, 2)).lightest).toEqual([255, 255, 255]); + const after = await sampleMergedCellPixels(editorPage, 1, 1, 2); + expectColorClose(after.darkest, [38, 38, 38]); + }); +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.ts b/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.ts new file mode 100644 index 0000000000..38975b0439 --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, frameEval } from '../../helpers'; +import { sampleCellPixels } from '../../utils/spreadsheet-editor'; + +test.describe('Spreadsheet editor - dark mode rendering', () => { + /* + TESTING MID-EDIT DARK-MODE TOGGLE BEHAVIOR (ACCEPTED, NOT A BUG) + */ + test('a cell actively being edited does not live-update on a mid-edit dark-mode toggle', async ({ page }) => { + // Neither the cell editor's text nor its background updates live if + // dark mode is toggled mid-edit -- both are one-time snapshots taken + // when editing starts. This is intentional (a live-refresh fix was + // prototyped and deliberately reverted as not worth it for this narrow, + // self-correcting gap), so this pins the current behavior as a + // regression guard, not a gap to fix. + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + await editorPage.keyboard.press('Control+Home'); + await editorPage.keyboard.type('hello'); + + const ceColors = () => + frameEval(editorPage, (win) => { + const canvas = win.document.getElementById('ce-canvas') as HTMLCanvasElement; + const ctx = canvas.getContext('2d')!; + const img = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + const set = new Set(); + for (let i = 0; i < img.length; i += 4) set.add(img[i] + ',' + img[i + 1] + ',' + img[i + 2]); + return Array.from(set).sort(); + }); + + const before = await ceColors(); + + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + // Confirm the toggle is genuinely in effect elsewhere (the main grid), + // so a lack of change in ce-canvas below can't be mistaken for the + // toggle simply not having landed yet. + await expect.poll(() => sampleCellPixels(editorPage, 5, 5).then((s) => s.darkest)).not.toEqual([255, 255, 255]); + + const after = await ceColors(); + expect(after).toEqual(before); + + // ENDING THE EDIT AND STARTING A NEW ONE DOES PICK UP THE NEW THEME + await editorPage.keyboard.press('Escape'); + await editorPage.keyboard.press('Control+Home'); + await editorPage.keyboard.type('world'); + await expect.poll(async () => (await ceColors()).includes('38,38,38')).toBe(true); + }); +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/print-preview.ts b/e2e/tests/spreadsheeteditor/darkmode/print-preview.ts new file mode 100644 index 0000000000..1f17a454fe --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/print-preview.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, frameEval, expectColorClose } from '../../helpers'; + +test.describe('Spreadsheet editor - dark mode print', () => { + /* + TESTING PRINT PREVIEW STAYS LIGHT REGARDLESS OF CONTENT DARK MODE + */ + test('print preview stays in light mode regardless of content dark mode', async ({ page }) => { + // Print already renders through a rendering context with dark mode + // forced off. This confirms the print-preview canvas background stays + // white even with content dark mode on, using a cell whose text + // overflows across several empty neighboring cells -- a layout that + // once leaked dark-theme colors into print at the cell boundaries. + const { editorPage, frame } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(1, 2, 1, 2).setValue('automatic borders colors here overflowing text'); + }); + + // SWITCH TO DARK MODE + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + await expect.poll(() => editorApi(editorPage, (api) => api.isDarkMode)).toBe(true); + + // OPEN PRINT PREVIEW (FILE TAB -> "PRINT WITH PREVIEW") + + // Drives the real UI, not asc_initPrintPreview directly -- the panel's + // container element that the API call needs only exists via this flow. + await frame.locator('a[data-tab="file"]').click(); + await frame.locator('#fm-btn-print-with-preview').click(); + + // The preview canvas is created lazily once the panel shows -- wait for + // it rather than assume it's there immediately after the click. + const previewCanvas = frame.locator('#print-preview-canvas'); + await expect(previewCanvas).toBeAttached({ timeout: 10_000 }); + + // MEASURE PRINT PREVIEW BACKGROUND + + const corners = await frameEval(editorPage, (win) => { + const canvas = win.document.getElementById('print-preview-canvas') as HTMLCanvasElement; + const ctx = canvas.getContext('2d')!; + const px = (x: number, y: number) => Array.from(ctx.getImageData(x, y, 1, 1).data.slice(0, 3)); + return { topLeft: px(2, 2), bottomRight: px(canvas.width - 3, canvas.height - 3) }; + }); + + expectColorClose(corners.topLeft, [255, 255, 255]); + expectColorClose(corners.bottomRight, [255, 255, 255]); + + // TODO: this only checks the page background stays light, not that the + // specific black-bars artifact 5.2 fixed is absent at the cell-boundary + // lines the overflowing text crosses. A full regression guard for that + // would need to know the exact print-preview pixel coordinates of each + // cell boundary within the overflowing row, not yet worked out. + }); +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/resize-guide.ts b/e2e/tests/spreadsheeteditor/darkmode/resize-guide.ts new file mode 100644 index 0000000000..fb16382c62 --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/resize-guide.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, expectColorClose } from '../../helpers'; +import { resizeColumnGuideColor } from '../../utils/spreadsheet-editor'; + +test.describe('Spreadsheet editor - dark mode rendering', () => { + /* + TESTING COLUMN RESIZE GUIDE VISIBILITY IN DARK MODE + */ + test('column resize guide is visible against the dark canvas, not near-black-on-black', async ({ page }) => { + // The resize-drag guide line was hardcoded to black, invisible against a + // dark canvas. This confirms it now uses a theme-aware color instead. + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + // Select a cell in a different column first: drawing the guide also + // repaints the current selection marquee, which would otherwise + // contaminate the sampled column if selection were nearby. + await editorApi(editorPage, (api) => api.asc_findCell('H2')); + + // MEASURE GUIDE COLOR IN LIGHT MODE + expectColorClose((await resizeColumnGuideColor(editorPage, 1))!, [0, 0, 0]); + + // SWITCH TO DARK MODE + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + + // MEASURE GUIDE COLOR IN DARK MODE + await expect.poll(() => resizeColumnGuideColor(editorPage, 1)).toEqual([204, 204, 204]); + }); + + // TODO: row resize (drawRowGuides) not covered here -- only the column + // guide (drawColumnGuides) was verified. The two are structurally + // near-identical in source, but that symmetry hasn't actually been + // checked live per this file's own working principle (don't assume + // symmetry between sides/axes without verifying each one). +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/search-highlight.ts b/e2e/tests/spreadsheeteditor/darkmode/search-highlight.ts new file mode 100644 index 0000000000..6d4ee5d1b3 --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/search-highlight.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi } from '../../helpers'; +import { sampleCellPixels } from '../../utils/spreadsheet-editor'; + +test.describe('Spreadsheet editor - dark mode rendering', () => { + /* + TESTING SEARCH-HIGHLIGHT TEXT CONTRAST IN DARK MODE + */ + test('search-highlighted cell text stays readable in dark mode', async ({ page }) => { + // Automatic text on a search-highlighted cell must stay dark/readable + // against the highlight's own light yellow fill, in both light and dark + // mode -- not inverted to light just because dark mode is on. + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + // Short text that stays within one cell's width -- longer text overflows + // into the empty neighbor cell, leaving no glyph ink to sample within + // this cell's own bounds. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(1, 1, 1, 1).setValue('hi'); + }); + + await editorApi(editorPage, (api) => { + const options = new (window as any).Asc.asc_CFindOptions(); + options.asc_setFindWhat('hi'); + options.asc_setScanForward(true); + options.asc_setIsMatchCase(false); + options.asc_setIsWholeCell(false); + options.asc_setScanOnOnlySheet((window as any).Asc.c_oAscSearchBy.Sheet); + options.asc_setScanByRows(true); + options.asc_setLookIn((window as any).Asc.c_oAscFindLookIn.Value); + api.asc_findText(options); + api.asc_selectSearchingResults(true); + // Unlike a plain setValue(), the search calls don't trigger their own + // repaint -- without this, the cell samples as a uniform fill color + // with no glyph ink at all. + const ws = api.wb.getWorksheet(); + ws._cleanCellsTextMetricsCache(); + ws.draw(); + }); + + // Poll for the highlight to actually be showing rather than assume it + // landed synchronously. + const luminanceSum = (rgb: number[]) => rgb[0] + rgb[1] + rgb[2]; + await expect.poll(async () => luminanceSum((await sampleCellPixels(editorPage, 1, 1)).lightest)).toBeGreaterThan(400); + + // MEASURE TEXT/HIGHLIGHT CONTRAST IN LIGHT MODE + + const before = await sampleCellPixels(editorPage, 1, 1); + expect(luminanceSum(before.darkest)).toBeLessThan(100); + expect(luminanceSum(before.lightest)).toBeGreaterThan(400); + + // SWITCH TO DARK MODE + + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + + // MEASURE TEXT/HIGHLIGHT CONTRAST IN DARK MODE + + // Text must stay dark/readable; the highlight fill stays a light color + // too (shifted to a muted yellow, not inverted to a dark canvas gray). + await expect.poll(async () => luminanceSum((await sampleCellPixels(editorPage, 1, 1)).darkest)).toBeLessThan(100); + const after = await sampleCellPixels(editorPage, 1, 1); + expect(luminanceSum(after.darkest)).toBeLessThan(100); + expect(luminanceSum(after.lightest)).toBeGreaterThan(300); + }); +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/toggle-button.ts b/e2e/tests/spreadsheeteditor/darkmode/toggle-button.ts new file mode 100644 index 0000000000..9e6306e81b --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/toggle-button.ts @@ -0,0 +1,45 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, frameEval } from '../../helpers'; + +test.describe('Spreadsheet editor - dark mode', () => { + /* + TESTING THE DARK DOCUMENT TOGGLE BUTTON + */ + test('toggling content dark mode flips the api flag and syncs the toolbar button', async ({ page }) => { + // The button is locked until the interface theme is dark. This clicks + // the real button (not the API directly) in both directions and checks + // its enabled/active state stays in sync with the underlying flag. + const { editorPage, frame } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + await frame.locator('a[data-tab="view"]').click(); + + const darkDocButton = frame.locator('#slot-btn-dark-document button').first(); + + // BUTTON STARTS DISABLED IN A FRESH (LIGHT-THEME) DOCUMENT + await expect(darkDocButton).toHaveClass(/disabled/); + expect(await editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); + + // SWITCH INTERFACE THEME TO DARK -- BUTTON BECOMES ENABLED + await frameEval(editorPage, (win) => win.Common.UI.Themes.setTheme('theme-night')); + + await expect(darkDocButton).not.toHaveClass(/disabled/); + expect(await editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); + + // CLICK THE BUTTON TO ENABLE DARK MODE + await darkDocButton.click(); + + await expect.poll(() => editorApi(editorPage, (api) => api.isDarkMode)).toBe(true); + await expect(darkDocButton).toHaveClass(/active/); + + // CLICK AGAIN TO SWITCH BACK TO LIGHT MODE + + // The click handler debounces rapid clicks for 500ms; a second click + // inside that window is dropped rather than toggling again. This wait + // is matched to that real, named constant, not a guess at timing. + await editorPage.waitForTimeout(600); + await darkDocButton.click(); + + await expect.poll(() => editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); + await expect(darkDocButton).not.toHaveClass(/active/); + }); +}); diff --git a/e2e/tests/utils/spreadsheet-editor.ts b/e2e/tests/utils/spreadsheet-editor.ts new file mode 100644 index 0000000000..88d5e94940 --- /dev/null +++ b/e2e/tests/utils/spreadsheet-editor.ts @@ -0,0 +1,156 @@ +import { Page } from '@playwright/test'; +import { frameEval } from '../helpers'; + +// row/col are passed through frameEval's `args` (real Playwright-serialized +// values, arrived as genuine function parameters), not baked into a string -- +// see helpers.ts's frameEval for how that avoids the closure-doesn't-survive- +// serialization problem without writing code as text. +export function cellFillRgb(editorPage: Page, row: number, col: number) { + return frameEval(editorPage, (win, row: number, col: number) => { + const fill = win.Asc.editor.wb.getWorksheet().model.getRange3(row, col, row, col).getFill(); + return fill && fill.patternFill && fill.patternFill.fgColor ? fill.patternFill.fgColor.rgb : null; + }, [row, col]); +} + +export function cellFontRgb(editorPage: Page, row: number, col: number) { + return frameEval(editorPage, (win, row: number, col: number) => { + const color = win.Asc.editor.wb.getWorksheet().model.getRange3(row, col, row, col).getFont().getColor(); + return color ? color.rgb : null; + }, [row, col]); +} + +// Samples the darkest/lightest pixel within a cell's rendered rect, rather +// than a single fixed coordinate -- robust to exact glyph/font-metric +// position, which a one-shot pixel guess is not. +// +// The scanned region is inset from each edge of the cell's rect by the +// cell's actual border width (read from the model via getBorderFull(), +// which reports 0/1/2/3px for None/Thin/Medium/Thick -- WorkbookElems.js's +// BorderProp.setStyle), or 1px if the cell has no explicit border on that +// side -- the default gridline is hardcoded to exactly 1px in +// WorksheetView.prototype._drawGrid regardless of any cell's own border. +// This isn't a guessed margin: confirmed via browser-loop against two real +// contamination modes at the raw rect boundary -- (1) an unfilled cell's +// own last row/column can literally *be* the gridline pixel, and (2) two +// vertically adjacent cells' paint rects can meet such that the shared +// boundary row reads as the *next* cell's fill rather than this one's -- +// and the derived inset value (1px, for a cell with no explicit border) +// clears both exactly as well as an earlier, uninvestigated flat guess did. +// A scenario that specifically wants to measure the gridline/border color +// itself (see TASKS/5-dark-theme-canvas-background/5.8-e2e-dark-mode-coverage, +// scenario 2) needs the opposite: sample at/near the edge on purpose, not +// inset from it. +export function sampleCellPixels(editorPage: Page, row: number, col: number) { + return frameEval(editorPage, (win, row: number, col: number) => { + const ws = win.Asc.editor.wb.getWorksheet(); + const range = ws.model.getRange3(row, col, row, col); + const border = range.getBorderFull(); + const inset = Math.max(1, border.t.w, border.r.w, border.b.w, border.l.w); + const rect = ws.getCellCoord(col, row); + const x = Math.round(rect._x) + inset; + const y = Math.round(rect._y) + inset; + const w = Math.round(rect._width) - 2 * inset; + const h = Math.round(rect._height) - 2 * inset; + const ctx = win.document.getElementById('ws-canvas').getContext('2d'); + const img = ctx.getImageData(x, y, w, h).data; + let darkest = [255, 255, 255]; + let darkestLum = Infinity; + let lightest = [0, 0, 0]; + let lightestLum = -Infinity; + for (let i = 0; i < img.length; i += 4) { + const lum = img[i] + img[i + 1] + img[i + 2]; + if (lum < darkestLum) { darkestLum = lum; darkest = [img[i], img[i + 1], img[i + 2]]; } + if (lum > lightestLum) { lightestLum = lum; lightest = [img[i], img[i + 1], img[i + 2]]; } + } + return { darkest, lightest }; + }, [row, col]); +} + +// Samples the single pixel row a cell's own bottom border renders at. +// Confirmed via browser-loop, not assumed: for a Thin border (the only +// width tested so far), the border occupies exactly one row at +// `rect._y + rect._height - 1`, with plain background on every row above +// and below it -- no inset/tolerance needed the way sampleCellPixels needs +// for fill/text, since there's no adjacent-cell-bleed risk at this specific +// coordinate for a bottom border between two unfilled cells. Only the +// bottom side is implemented -- top/left/right were not verified to sit at +// the equivalent offset (the fill/gridline investigation for +// sampleCellPixels found left/top edges behave differently from +// right/bottom ones, so don't assume symmetry without checking each side +// live first). +export function bottomBorderPixel(editorPage: Page, row: number, col: number) { + return frameEval(editorPage, (win, row: number, col: number) => { + const ws = win.Asc.editor.wb.getWorksheet(); + const rect = ws.getCellCoord(col, row); + const x = Math.round(rect._x) + 5; + const y = Math.round(rect._y) + Math.round(rect._height) - 1; + const ctx = win.document.getElementById('ws-canvas').getContext('2d'); + const d = ctx.getImageData(x, y, 1, 1).data; + return [d[0], d[1], d[2]]; + }, [row, col]); +} + +// Same darkest/lightest-in-rect technique as sampleCellPixels, but across a +// horizontally merged cell's full combined width. getCellCoord itself is not +// merge-aware -- merging via asc_mergeCells changes the model (confirmed via +// browser-loop: ws.model.getMergedByCell reports the merged bbox correctly) +// but getCellCoord(col1, row) still returns only the anchor column's own +// (unmerged) width. The combined rect is computed manually: left edge from +// the anchor cell (col1), right edge from the last cell in the merge +// (col2)'s own right edge -- confirmed live to span the full merged area +// with no leftover internal column-boundary artifact. Only horizontal +// (same-row, multi-column) merges are implemented/verified; vertical merges +// were not tested. +export function sampleMergedCellPixels(editorPage: Page, row: number, col1: number, col2: number) { + return frameEval(editorPage, (win, row: number, col1: number, col2: number) => { + const ws = win.Asc.editor.wb.getWorksheet(); + const anchorBorder = ws.model.getRange3(row, col1, row, col1).getBorderFull(); + const inset = Math.max(1, anchorBorder.t.w, anchorBorder.r.w, anchorBorder.b.w, anchorBorder.l.w); + const rectStart = ws.getCellCoord(col1, row); + const rectEnd = ws.getCellCoord(col2, row); + const x = Math.round(rectStart._x) + inset; + const y = Math.round(rectStart._y) + inset; + const w = Math.round(rectEnd._x) + Math.round(rectEnd._width) - Math.round(rectStart._x) - 2 * inset; + const h = Math.round(rectStart._height) - 2 * inset; + const ctx = win.document.getElementById('ws-canvas').getContext('2d'); + const img = ctx.getImageData(x, y, w, h).data; + let darkest = [255, 255, 255]; + let darkestLum = Infinity; + let lightest = [0, 0, 0]; + let lightestLum = -Infinity; + for (let i = 0; i < img.length; i += 4) { + const lum = img[i] + img[i + 1] + img[i + 2]; + if (lum < darkestLum) { darkestLum = lum; darkest = [img[i], img[i + 1], img[i + 2]]; } + if (lum > lightestLum) { lightestLum = lum; lightest = [img[i], img[i + 1], img[i + 2]]; } + } + return { darkest, lightest }; + }, [row, col1, col2]); +} + +// Draws the column-resize drag guide (WorksheetView.prototype.drawColumnGuides +// -- the dotted vertical line shown while dragging a column border) directly, +// without simulating a real mouse drag, and samples its color. Confirmed via +// browser-loop: the guide draws to the *overlay* canvas (`ws-canvas-overlay`, +// this.overlayCtx in the source), not the main `ws-canvas` every other helper +// in this file samples -- a different canvas entirely. It's a 1px-wide +// dotted vertical line at `colLeft - 1`, dots landing on every odd row (0, +// 2, 4... are transparent gaps) -- scans a tall-enough strip to guarantee +// catching an "on" dot rather than gambling on one row. Calling +// drawColumnGuides also repaints the current selection's marquee as a side +// effect (WorksheetView.prototype._drawSelection, called internally right +// before the guide itself) -- confirmed this can contaminate the scan if the +// active cell's marquee happens to overlap the sampled column, so the caller +// should select a cell in a visibly different column first. +export function resizeColumnGuideColor(editorPage: Page, col: number) { + return frameEval(editorPage, (win, col: number) => { + const ws = win.Asc.editor.wb.getWorksheet(); + const colLeft = ws._getColLeft(col); + ws.drawColumnGuides(col, colLeft, 0, 0); + const ctx = win.document.getElementById('ws-canvas-overlay').getContext('2d'); + const img = ctx.getImageData(Math.round(colLeft) - 1, 0, 1, 30).data; + for (let i = 0; i < img.length; i += 4) { + if (img[i + 3] > 0) return [img[i], img[i + 1], img[i + 2]]; + } + return null; + }, [col]); +} From de7bcf5c6feb8a63bd4c5e591189d2570bdd215b Mon Sep 17 00:00:00 2001 From: j-base64 Date: Tue, 28 Jul 2026 17:04:49 +0200 Subject: [PATCH 3/8] test(e2e): refactor spreadsheeteditor dark-mode tests (drop test index in favor of plain spec files) Signed-off-by: j-base64 Assisted-by: ClaudeCode:claude-sonnet-5 --- .../darkmode/README.md} | 71 +++++++++---------- .../{cell-borders.ts => cell-borders.spec.ts} | 0 .../{cell-colors.ts => cell-colors.spec.ts} | 50 +++++++++---- ...rface-theme.ts => interface-theme.spec.ts} | 0 .../{merged-cells.ts => merged-cells.spec.ts} | 0 ...edit-toggle.ts => mid-edit-toggle.spec.ts} | 0 ...print-preview.ts => print-preview.spec.ts} | 0 .../{resize-guide.ts => resize-guide.spec.ts} | 0 ...-highlight.ts => search-highlight.spec.ts} | 0 ...toggle-button.ts => toggle-button.spec.ts} | 0 10 files changed, 71 insertions(+), 50 deletions(-) rename e2e/tests/{spreadsheeteditor-darkmode.spec.ts => spreadsheeteditor/darkmode/README.md} (50%) rename e2e/tests/spreadsheeteditor/darkmode/{cell-borders.ts => cell-borders.spec.ts} (100%) rename e2e/tests/spreadsheeteditor/darkmode/{cell-colors.ts => cell-colors.spec.ts} (67%) rename e2e/tests/spreadsheeteditor/darkmode/{interface-theme.ts => interface-theme.spec.ts} (100%) rename e2e/tests/spreadsheeteditor/darkmode/{merged-cells.ts => merged-cells.spec.ts} (100%) rename e2e/tests/spreadsheeteditor/darkmode/{mid-edit-toggle.ts => mid-edit-toggle.spec.ts} (100%) rename e2e/tests/spreadsheeteditor/darkmode/{print-preview.ts => print-preview.spec.ts} (100%) rename e2e/tests/spreadsheeteditor/darkmode/{resize-guide.ts => resize-guide.spec.ts} (100%) rename e2e/tests/spreadsheeteditor/darkmode/{search-highlight.ts => search-highlight.spec.ts} (100%) rename e2e/tests/spreadsheeteditor/darkmode/{toggle-button.ts => toggle-button.spec.ts} (100%) diff --git a/e2e/tests/spreadsheeteditor-darkmode.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/README.md similarity index 50% rename from e2e/tests/spreadsheeteditor-darkmode.spec.ts rename to e2e/tests/spreadsheeteditor/darkmode/README.md index 2ae97a27f0..43cd0b0dea 100644 --- a/e2e/tests/spreadsheeteditor-darkmode.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/README.md @@ -1,6 +1,4 @@ -/* - - +``` /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$$ /$$ /$$__ $$ /$$__ $$| $$_____/ | $$__ $$ | $$ | $$ \__/| $$ \__/| $$ | $$ \ $$ /$$$$$$ /$$$$$$ | $$ /$$ @@ -17,60 +15,57 @@ | $$\ $ | $$| $$ | $$| $$ | $$| $$_____/ | $$| $$_____/ \____ $$ | $$ /$$ | $$ \/ | $$| $$$$$$/| $$$$$$$| $$$$$$$ | $$| $$$$$$$ /$$$$$$$/ | $$$$/ |__/ |__/ \______/ \_______/ \_______/ |__/ \_______/|_______/ \___/ +``` - - -what we want to test +## what we want to test UI and relation with darkmode -- button darkmode beeing dsiabled in light theme -- button darkmode beeing enabled in dark theme +- 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 + usertextcolor , 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 +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" -// Index for the spreadsheet dark-mode suite -- imports each test file under -// spreadsheeteditor/darkmode/ for its side effect (registering its test() -// calls). Those files use a plain .ts extension (not .spec.ts/.test.ts) so -// Playwright's own file discovery doesn't also pick them up directly and -// run them a second time. -// -// Run this suite with --workers=1, e.g.: -// npx playwright test tests/spreadsheeteditor-darkmode.spec.ts --workers=1 -// Splitting into one file per test lets Playwright schedule up to ~7 -// workers in parallel by default on this machine (cpus/2). Confirmed live: -// running 3 grouped files already used 3 workers and produced page.goto -// navigation timeouts against the shared dev container under that -// concurrent load -- 9 separate files would make more workers available, -// not fewer. Forcing one worker keeps these tests running against the -// shared container one at a time, like the rest of this suite already -// does when run as a single file. +# everything except one +npx playwright test tests/spreadsheeteditor/darkmode/ --grep-invert "resize guide" +``` -import './spreadsheeteditor/darkmode/toggle-button'; -import './spreadsheeteditor/darkmode/interface-theme'; -import './spreadsheeteditor/darkmode/cell-colors'; -import './spreadsheeteditor/darkmode/cell-borders'; -import './spreadsheeteditor/darkmode/merged-cells'; -import './spreadsheeteditor/darkmode/search-highlight'; -import './spreadsheeteditor/darkmode/resize-guide'; -import './spreadsheeteditor/darkmode/mid-edit-toggle'; -import './spreadsheeteditor/darkmode/print-preview'; +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-* +``` diff --git a/e2e/tests/spreadsheeteditor/darkmode/cell-borders.ts b/e2e/tests/spreadsheeteditor/darkmode/cell-borders.spec.ts similarity index 100% rename from e2e/tests/spreadsheeteditor/darkmode/cell-borders.ts rename to e2e/tests/spreadsheeteditor/darkmode/cell-borders.spec.ts diff --git a/e2e/tests/spreadsheeteditor/darkmode/cell-colors.ts b/e2e/tests/spreadsheeteditor/darkmode/cell-colors.spec.ts similarity index 67% rename from e2e/tests/spreadsheeteditor/darkmode/cell-colors.ts rename to e2e/tests/spreadsheeteditor/darkmode/cell-colors.spec.ts index bcea530467..77fda104e0 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/cell-colors.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/cell-colors.spec.ts @@ -7,16 +7,21 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { TESTING CORE CELL COLOR CORRECTION */ test('automatic cell colors invert in dark mode; explicit colors stay untouched', async ({ page }) => { - // Covers the four automatic/explicit combinations a cell's fill and text - // can be in: B2 (both automatic), B3 (fill only), B4 (both explicit), - // B6 (text only). Measures each before/after toggling dark mode. + // Covers the automatic/explicit combinations a cell's fill and text can + // be in: B2 (both automatic), B3 (dark fill only), B4 (both explicit), + // B6 (text only), B7 (light fill only). Measures each before/after + // toggling dark mode. Automatic text inverts for contrast against + // whichever background is actually behind it -- the dark canvas (B2), + // or its own cell's fill if it has one (B3, B7) -- but only when that + // background is dark enough to need it: B3's darker fill triggers the + // inversion, B7's lighter fill doesn't. const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); // Assert the light-mode starting point explicitly rather than assume it // -- every "before" check below relies on it. expect(await editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); - // WRITE B2/B3/B4/B6 + // WRITE B2/B3/B4/B6/B7 // B2: left on default/automatic colors -- no fill, no explicit text color. await editorApi(editorPage, (api) => { @@ -58,12 +63,23 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { }); await expect.poll(() => cellFontRgb(editorPage, 5, 1)).not.toBeNull(); + // B7: light explicit fill, automatic text. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(6, 1, 6, 1).setValue('lightBgOnly'); + }); + await editorApi(editorPage, (api) => { + api.asc_findCell('B7'); + api.asc_setCellBackgroundColor(new (window as any).Asc.asc_CColor(220, 220, 220)); + }); + await expect.poll(() => cellFillRgb(editorPage, 6, 1)).not.toBeNull(); + // MEASURE COLORS IN LIGHT MODE const b2Before = await sampleCellPixels(editorPage, 1, 1); const b3Before = await sampleCellPixels(editorPage, 2, 1); const b4Before = await sampleCellPixels(editorPage, 3, 1); const b6Before = await sampleCellPixels(editorPage, 5, 1); + const b7Before = await sampleCellPixels(editorPage, 6, 1); // B2's automatic text is literal black against the light-mode canvas. expectColorClose(b2Before.darkest, [0, 0, 0]); @@ -76,6 +92,10 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { // is plain white in light mode. expectColorClose(b6Before.darkest, [10, 10, 60]); expectColorClose(b6Before.lightest, [255, 255, 255]); + // B7's light explicit fill renders as set; its automatic text is literal + // black, same as B2's. + expectColorClose(b7Before.darkest, [0, 0, 0]); + expectColorClose(b7Before.lightest, [220, 220, 220]); // SWITCH TO DARK MODE @@ -90,18 +110,19 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { const b3After = await sampleCellPixels(editorPage, 2, 1); const b4After = await sampleCellPixels(editorPage, 3, 1); const b6After = await sampleCellPixels(editorPage, 5, 1); + const b7After = await sampleCellPixels(editorPage, 6, 1); // Explicit fill/text colors are exactly unchanged by the dark-mode toggle. - expectColorClose(b3After.lightest, [200, 100, 50]); + expectColorClose(b3After.darkest, [200, 100, 50]); expectColorClose(b4After.darkest, [30, 30, 120]); expectColorClose(b4After.lightest, [255, 220, 0]); - // KNOWN BUG: B3's automatic text should invert for contrast in dark mode - // the same way B2's does, but any explicit fill -- light or dark -- - // currently forces it to stay literal black instead. This asserts - // today's actual (wrong) output on purpose: it starts failing the - // moment that bug is fixed, which is the signal to update it, not a - // regression to chase. - expectColorClose(b3After.darkest, [0, 0, 0]); + // B3's automatic text inverts for contrast against its own fill, same as + // B2 does against the dark canvas -- B3's fill (200,100,50) is dark + // enough (luminance ~124/255) to need the correction, so the text + // inverts to white. That swaps which sample lands in which slot: the + // fill (darker of the two) is now "darkest", the corrected text + // (lighter) is now "lightest" -- same swap seen below for B6. + expectColorClose(b3After.lightest, [255, 255, 255]); // B6's explicit text is still unchanged; its automatic background is now // the dark canvas gray instead of white -- the two swap which slot // (darkest/lightest) they land in, since the text color (sum 80) is @@ -109,5 +130,10 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { // dark-mode gray background (sum 114), so it's "darkest" in both modes. expectColorClose(b6After.darkest, [10, 10, 60]); expectColorClose(b6After.lightest, [38, 38, 38]); + // B7's fill (220,220,220) is light enough (luminance ~220/255, well + // above the threshold) that automatic text must stay untouched, exactly + // as in light mode -- no swap, no inversion. + expectColorClose(b7After.darkest, [0, 0, 0]); + expectColorClose(b7After.lightest, [220, 220, 220]); }); }); diff --git a/e2e/tests/spreadsheeteditor/darkmode/interface-theme.ts b/e2e/tests/spreadsheeteditor/darkmode/interface-theme.spec.ts similarity index 100% rename from e2e/tests/spreadsheeteditor/darkmode/interface-theme.ts rename to e2e/tests/spreadsheeteditor/darkmode/interface-theme.spec.ts diff --git a/e2e/tests/spreadsheeteditor/darkmode/merged-cells.ts b/e2e/tests/spreadsheeteditor/darkmode/merged-cells.spec.ts similarity index 100% rename from e2e/tests/spreadsheeteditor/darkmode/merged-cells.ts rename to e2e/tests/spreadsheeteditor/darkmode/merged-cells.spec.ts diff --git a/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.ts b/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.spec.ts similarity index 100% rename from e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.ts rename to e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.spec.ts diff --git a/e2e/tests/spreadsheeteditor/darkmode/print-preview.ts b/e2e/tests/spreadsheeteditor/darkmode/print-preview.spec.ts similarity index 100% rename from e2e/tests/spreadsheeteditor/darkmode/print-preview.ts rename to e2e/tests/spreadsheeteditor/darkmode/print-preview.spec.ts diff --git a/e2e/tests/spreadsheeteditor/darkmode/resize-guide.ts b/e2e/tests/spreadsheeteditor/darkmode/resize-guide.spec.ts similarity index 100% rename from e2e/tests/spreadsheeteditor/darkmode/resize-guide.ts rename to e2e/tests/spreadsheeteditor/darkmode/resize-guide.spec.ts diff --git a/e2e/tests/spreadsheeteditor/darkmode/search-highlight.ts b/e2e/tests/spreadsheeteditor/darkmode/search-highlight.spec.ts similarity index 100% rename from e2e/tests/spreadsheeteditor/darkmode/search-highlight.ts rename to e2e/tests/spreadsheeteditor/darkmode/search-highlight.spec.ts diff --git a/e2e/tests/spreadsheeteditor/darkmode/toggle-button.ts b/e2e/tests/spreadsheeteditor/darkmode/toggle-button.spec.ts similarity index 100% rename from e2e/tests/spreadsheeteditor/darkmode/toggle-button.ts rename to e2e/tests/spreadsheeteditor/darkmode/toggle-button.spec.ts From 9255efd065ef2e8078206303167c57e2ba74709f Mon Sep 17 00:00:00 2001 From: j-base64 Date: Wed, 29 Jul 2026 14:49:11 +0200 Subject: [PATCH 4/8] test(e2e): add gradient-fill cell-editor dark-mode text-contrast coverage Signed-off-by: j-base64 Assisted-by: ClaudeCode:claude-sonnet-5 --- .../gradient-fill-cell-editor.spec.ts | 88 +++++++++++++++++++ e2e/tests/utils/spreadsheet-editor.ts | 12 +++ 2 files changed, 100 insertions(+) create mode 100644 e2e/tests/spreadsheeteditor/darkmode/gradient-fill-cell-editor.spec.ts diff --git a/e2e/tests/spreadsheeteditor/darkmode/gradient-fill-cell-editor.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/gradient-fill-cell-editor.spec.ts new file mode 100644 index 0000000000..595bf097ae --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/gradient-fill-cell-editor.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, frameEval } from '../../helpers'; +import { cellHasGradientFill } from '../../utils/spreadsheet-editor'; + +test.describe('Spreadsheet editor - dark mode rendering', () => { + /* + GRADIENT-FILLED CELL, EDITED IN DARK MODE: TEXT MUST STAY READABLE + + The cell editor never actually renders a gradient/pattern fill behind the + text being edited (a separate, pre-existing simplification) -- it falls + back to painting the theme-resolved cells.defaultState.background instead, + which does track dark mode (white in light mode, #262626 in dark mode). + Before the fix, the automatic-text-color decision didn't know that + fallback had happened and kept its grid-path "unknown gradient contrast, + don't touch the text" exemption regardless -- so the text rendered at its + literal stored black, unreadable against the now-dark fallback background. + See TASKS/5-dark-theme-canvas-background/5.10-gradient-fill-editor-black-text. + */ + test('editing a gradient-filled cell keeps automatic text readable against the editor background', async ({ page }) => { + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + expect(await editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); + + // B2: a value, and a gradient fill with no patternFill at all -- the + // specific shape that makes Fill.prototype.bg()/getSolidFill() return + // null (both only ever look at patternFill), which is what leads the + // editor to fall back to cells.defaultState.background in the first place. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(1, 1, 1, 1).setValue('gradient'); + api.asc_findCell('B2'); + }); + + await editorApi(editorPage, (api) => { + const w = window as any; + const fill = new w.Asc.asc_CFill2(); + const gradient = new w.Asc.asc_CGradientFill(); + const stop1 = new w.Asc.asc_CGradientStop(); + stop1.asc_setColor(new w.Asc.asc_CColor(80, 80, 200)); + stop1.asc_setPosition(0); + const stop2 = new w.Asc.asc_CGradientStop(); + stop2.asc_setColor(new w.Asc.asc_CColor(20, 20, 80)); + stop2.asc_setPosition(1); + gradient.asc_putGradientStops([stop1, stop2]); + fill.asc_setGradientFill(gradient); + api.asc_setCellFill(fill); + }); + await expect.poll(() => cellHasGradientFill(editorPage, 1, 1)).toBe(true); + + const ceColors = () => + frameEval(editorPage, (win) => { + const canvas = win.document.getElementById('ce-canvas') as HTMLCanvasElement; + const ctx = canvas.getContext('2d')!; + const img = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + const set = new Set(); + for (let i = 0; i < img.length; i += 4) set.add(img[i] + ',' + img[i + 1] + ',' + img[i + 2]); + return Array.from(set).sort(); + }); + + // START EDITING B2 IN LIGHT MODE -- ALREADY-WORKING BASELINE + + await editorPage.keyboard.press('F2'); + const lightColors = await ceColors(); + // Pre-existing, unrelated limitation: the editor shows a plain white + // background, not the actual gradient -- confirmed here as the baseline + // this test builds on, not something this fix changes. + expect(lightColors).toContain('255,255,255'); + // Automatic text renders black against that white background -- readable. + expect(lightColors).toContain('0,0,0'); + await editorPage.keyboard.press('Escape'); + + // SWITCH TO DARK MODE, START EDITING B2 AGAIN + + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + await editorApi(editorPage, (api) => api.asc_findCell('B2')); + await editorPage.keyboard.press('F2'); + + const darkColors = await ceColors(); + // The editor's fallback background is cells.defaultState.background's + // dark-mode value (#262626 = 38,38,38) -- confirms the fallback is in + // effect, same as the no-fill case already covered by cell-colors.spec.ts. + expect(darkColors).toContain('38,38,38'); + // Before the fix: automatic text stayed literal black (0,0,0) here, + // unreadable against the 38,38,38 background. After the fix: the + // gradient-fallback case is checked against that now-known background + // like any other, so it gets corrected to a light color instead. + expect(darkColors).not.toContain('0,0,0'); + }); +}); diff --git a/e2e/tests/utils/spreadsheet-editor.ts b/e2e/tests/utils/spreadsheet-editor.ts index 88d5e94940..35759c53f5 100644 --- a/e2e/tests/utils/spreadsheet-editor.ts +++ b/e2e/tests/utils/spreadsheet-editor.ts @@ -12,6 +12,18 @@ export function cellFillRgb(editorPage: Page, row: number, col: number) { }, [row, col]); } +// Distinct from cellFillRgb: a gradient fill has no patternFill at all (the +// model's Fill keeps gradientFill and patternFill as separate, mutually-set +// members), so cellFillRgb's patternFill-only check always reports null for +// it -- this checks the gradientFill member directly, purely to confirm the +// fill landed on the model before proceeding, not to read its color. +export function cellHasGradientFill(editorPage: Page, row: number, col: number) { + return frameEval(editorPage, (win, row: number, col: number) => { + const fill = win.Asc.editor.wb.getWorksheet().model.getRange3(row, col, row, col).getFill(); + return !!(fill && fill.gradientFill); + }, [row, col]); +} + export function cellFontRgb(editorPage: Page, row: number, col: number) { return frameEval(editorPage, (win, row: number, col: number) => { const color = win.Asc.editor.wb.getWorksheet().model.getRange3(row, col, row, col).getFont().getColor(); From 436ccba9d5736ddb0679492ea1b9ff4a4ebfd365 Mon Sep 17 00:00:00 2001 From: j-base64 Date: Wed, 29 Jul 2026 14:50:20 +0200 Subject: [PATCH 5/8] refactor(e2e): clarify and extend button click timeout hack (already present in initial codebase) Signed-off-by: j-base64 Assisted-by: ClaudeCode:claude-sonnet-5 --- e2e/tests/spreadsheeteditor/darkmode/toggle-button.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/spreadsheeteditor/darkmode/toggle-button.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/toggle-button.spec.ts index 9e6306e81b..6f2d127cab 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/toggle-button.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/toggle-button.spec.ts @@ -36,7 +36,7 @@ test.describe('Spreadsheet editor - dark mode', () => { // The click handler debounces rapid clicks for 500ms; a second click // inside that window is dropped rather than toggling again. This wait // is matched to that real, named constant, not a guess at timing. - await editorPage.waitForTimeout(600); + await editorPage.waitForTimeout(500+250); await darkDocButton.click(); await expect.poll(() => editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); From 291832ffce5e81773a9a16a4c9a7c085f141dc50 Mon Sep 17 00:00:00 2001 From: j-base64 Date: Thu, 30 Jul 2026 17:22:41 +0200 Subject: [PATCH 6/8] test(e2e): add page-break-preview and pattern-fill dark-mode coverage Signed-off-by: j-base64 Assisted-by: ClaudeCode:claude-sonnet-5 --- .../darkmode/page-break-preview.spec.ts | 59 ++++++++++++++ ...ern-and-gradient-fill-cell-editor.spec.ts} | 78 ++++++++++++++++++- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 e2e/tests/spreadsheeteditor/darkmode/page-break-preview.spec.ts rename e2e/tests/spreadsheeteditor/darkmode/{gradient-fill-cell-editor.spec.ts => pattern-and-gradient-fill-cell-editor.spec.ts} (53%) diff --git a/e2e/tests/spreadsheeteditor/darkmode/page-break-preview.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/page-break-preview.spec.ts new file mode 100644 index 0000000000..3849f94219 --- /dev/null +++ b/e2e/tests/spreadsheeteditor/darkmode/page-break-preview.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@playwright/test'; +import { openNewEditor, editorApi, frameEval, expectColorClose } from '../../helpers'; + +test.describe('Spreadsheet editor - dark mode rendering', () => { + /* + PAGE BREAK PREVIEW, DARK MODE: OUTSIDE-PRINT-AREA OVERLAY MUST MATCH THE GRID-LINE COLOR + + Page Break Preview shades the area outside the print range using + cells.defaultState.border -- the same theme-resolved color every ordinary + grid line already uses (drawn via setStrokeStyle, never dark-mode + corrected). The overlay itself is painted through drawFillCell instead, + which re-applies getDarkModeCorrectedColor unless bIsExplicitFill says + otherwise. Before the fix, that flag came out false in the main + _drawRowBG loop, and wasn't passed at all for merged-cell sub-rectangles + -- so the already-resolved border color got corrected a second time, + landing a near-white overlay next to dark-grey grid lines in dark mode. + See TASKS/10-build-pr-endsession-sanitizer for the diagnosis. + */ + test('page-break-preview overlay color matches the grid-line border color in dark mode', async ({ page }) => { + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + // A single filled cell defines the print area (A1:B2) -- every other + // visible cell lands outside the print area once Page Break Preview is + // on, which is exactly the overlay this test targets. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(1, 1, 1, 1).setValue('hi'); + }); + + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + await editorApi(editorPage, (api) => + api.asc_SetSheetViewType((window as any).Asc.c_oAscESheetViewType.pageBreakPreview), + ); + + const sample = () => + frameEval(editorPage, (win) => { + const ws = win.Asc.editor.wb.getWorksheet(); + // J20 -- well outside the A1:B2 print range, still on-screen at + // the default zoom/window size. + const col = 9; + const row = 19; + const rect = ws.getCellCoord(col, row); + const ctx = win.document.getElementById('ws-canvas').getContext('2d'); + const d = ctx.getImageData(Math.round(rect._x) + 3, Math.round(rect._y) + 3, 1, 1).data; + const border = ws.settings.cells.defaultState.border; + return { + outsideAreaPixel: [d[0], d[1], d[2]], + expectedBorder: [border.getR(), border.getG(), border.getB()], + isOutsidePrintArea: ws.pagesModeDataContains(col, row) === false, + }; + }); + + // Poll: the print-page layout is computed lazily on the first grid + // draw after switching view type, not synchronously on the API call. + await expect.poll(async () => (await sample()).isOutsidePrintArea).toBe(true); + + const { outsideAreaPixel, expectedBorder } = await sample(); + expectColorClose(outsideAreaPixel, expectedBorder); + }); +}); diff --git a/e2e/tests/spreadsheeteditor/darkmode/gradient-fill-cell-editor.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts similarity index 53% rename from e2e/tests/spreadsheeteditor/darkmode/gradient-fill-cell-editor.spec.ts rename to e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts index 595bf097ae..1c150d9c55 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/gradient-fill-cell-editor.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test'; import { openNewEditor, editorApi, frameEval } from '../../helpers'; -import { cellHasGradientFill } from '../../utils/spreadsheet-editor'; +import { cellFillRgb, cellHasGradientFill } from '../../utils/spreadsheet-editor'; test.describe('Spreadsheet editor - dark mode rendering', () => { /* @@ -85,4 +85,80 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { // like any other, so it gets corrected to a light color instead. expect(darkColors).not.toContain('0,0,0'); }); + + /* + PATTERN-FILLED CELL, EDITED IN DARK MODE: TEXT MUST STAY READABLE + + Unlike a gradient fill, a genuine pattern fill (patternType other than + None/Solid) makes Fill.prototype.bg() return non-null -- the pattern's + foreground color -- and that's exactly what the editor paints as a flat + background (background: bg || defaultState.background), raw, with no + dark-mode correction of its own. Before the fix, the automatic-text-color + decision only ever got a resolvedFallbackBg when bg was null, so a + pattern fill fell into the grid path's "unknown contrast" exemption even + though the actual painted color was fully known right there -- text + rendered at its literal stored black regardless of how dark the + pattern's foreground actually was. + */ + test('editing a pattern-filled cell keeps automatic text readable against its foreground color', async ({ page }) => { + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + expect(await editorApi(editorPage, (api) => api.isDarkMode)).toBe(false); + + // B3: a value, and a dark, genuinely-patterned fill (DarkHorizontal, + // not Solid) -- the shape that makes getSolidFill() null but bg() + // non-null, which is the specific mismatch this fix targets. + await editorApi(editorPage, (api) => { + api.wb.getWorksheet().model.getRange3(2, 1, 2, 1).setValue('pattern'); + api.asc_findCell('B3'); + }); + + await editorApi(editorPage, (api) => { + const w = window as any; + const fill = new w.Asc.asc_CFill2(); + const pattern = new w.Asc.asc_CPatternFill(); + pattern.asc_setType(w.Asc.c_oAscPatternType.DarkHorizontal); + pattern.asc_setFgColor(new w.Asc.asc_CColor(30, 30, 90)); + pattern.asc_setBgColor(new w.Asc.asc_CColor(10, 10, 30)); + fill.asc_setPatternFill(pattern); + api.asc_setCellFill(fill); + }); + await expect.poll(() => cellFillRgb(editorPage, 2, 1)).not.toBeNull(); + + const ceColors = () => + frameEval(editorPage, (win) => { + const canvas = win.document.getElementById('ce-canvas') as HTMLCanvasElement; + const ctx = canvas.getContext('2d')!; + const img = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + const set = new Set(); + for (let i = 0; i < img.length; i += 4) set.add(img[i] + ',' + img[i + 1] + ',' + img[i + 2]); + return Array.from(set).sort(); + }); + + // START EDITING B3 IN LIGHT MODE -- ALREADY-WORKING BASELINE + + await editorPage.keyboard.press('F2'); + const lightColors = await ceColors(); + // The editor paints the pattern's foreground color flat, raw -- not + // corrected in either mode, confirmed here as the baseline. + expect(lightColors).toContain('30,30,90'); + expect(lightColors).toContain('0,0,0'); + await editorPage.keyboard.press('Escape'); + + // SWITCH TO DARK MODE, START EDITING B3 AGAIN + + await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); + await editorApi(editorPage, (api) => api.asc_findCell('B3')); + await editorPage.keyboard.press('F2'); + + const darkColors = await ceColors(); + // Same foreground color as before -- the editor's background paint is + // mode-independent, unaffected by this fix. + expect(darkColors).toContain('30,30,90'); + // Before the fix: automatic text stayed literal black here too, + // unreadable against this dark fill. After the fix: the pattern's + // actual foreground color is checked like any other known background, + // so it gets corrected to a light color instead. + expect(darkColors).not.toContain('0,0,0'); + }); }); From d6ce96c9070a89d5cda573be268e0a3819e51bca Mon Sep 17 00:00:00 2001 From: j-base64 Date: Thu, 30 Jul 2026 18:32:54 +0200 Subject: [PATCH 7/8] test(e2e): add EditorSkins color-corruption regression test, trim comments, update isColorDark threshold Signed-off-by: j-base64 Assisted-by: ClaudeCode:claude-sonnet-5 --- .../darkmode/cell-colors.spec.ts | 21 +++++++-------- .../darkmode/interface-theme.spec.ts | 27 +++++++++++++++++++ .../darkmode/page-break-preview.spec.ts | 14 +++------- ...tern-and-gradient-fill-cell-editor.spec.ts | 14 +++------- 4 files changed, 44 insertions(+), 32 deletions(-) diff --git a/e2e/tests/spreadsheeteditor/darkmode/cell-colors.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/cell-colors.spec.ts index 77fda104e0..5312127e5c 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/cell-colors.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/cell-colors.spec.ts @@ -8,13 +8,13 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { */ test('automatic cell colors invert in dark mode; explicit colors stay untouched', async ({ page }) => { // Covers the automatic/explicit combinations a cell's fill and text can - // be in: B2 (both automatic), B3 (dark fill only), B4 (both explicit), - // B6 (text only), B7 (light fill only). Measures each before/after + // be in: B2 (both automatic), B3 (fill only), B4 (both explicit), B6 + // (text only), B7 (light fill only). Measures each before/after // toggling dark mode. Automatic text inverts for contrast against // whichever background is actually behind it -- the dark canvas (B2), // or its own cell's fill if it has one (B3, B7) -- but only when that - // background is dark enough to need it: B3's darker fill triggers the - // inversion, B7's lighter fill doesn't. + // background is dark enough to need it: B7's fill isn't, and neither + // is B3's, just under the current isColorDark cutoff. const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); // Assert the light-mode starting point explicitly rather than assume it @@ -113,16 +113,13 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { const b7After = await sampleCellPixels(editorPage, 6, 1); // Explicit fill/text colors are exactly unchanged by the dark-mode toggle. - expectColorClose(b3After.darkest, [200, 100, 50]); expectColorClose(b4After.darkest, [30, 30, 120]); expectColorClose(b4After.lightest, [255, 220, 0]); - // B3's automatic text inverts for contrast against its own fill, same as - // B2 does against the dark canvas -- B3's fill (200,100,50) is dark - // enough (luminance ~124/255) to need the correction, so the text - // inverts to white. That swaps which sample lands in which slot: the - // fill (darker of the two) is now "darkest", the corrected text - // (lighter) is now "lightest" -- same swap seen below for B6. - expectColorClose(b3After.lightest, [255, 255, 255]); + // B3's fill (200,100,50, HSL lightness 125) sits above the current + // isColorDark cutoff, so its automatic text stays untouched -- nothing + // in this cell differs from light mode. + expectColorClose(b3After.darkest, [0, 0, 0]); + expectColorClose(b3After.lightest, [200, 100, 50]); // B6's explicit text is still unchanged; its automatic background is now // the dark canvas gray instead of white -- the two swap which slot // (darkest/lightest) they land in, since the text color (sum 80) is diff --git a/e2e/tests/spreadsheeteditor/darkmode/interface-theme.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/interface-theme.spec.ts index f0bac47f10..7cfe9c9274 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/interface-theme.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/interface-theme.spec.ts @@ -26,4 +26,31 @@ test.describe('Spreadsheet editor - dark mode', () => { await frameEval(editorPage, (win) => win.Common.UI.Themes.setTheme('theme-night')); await expect.poll(() => editorApi(editorPage, (api) => api.isDarkMode)).toBe(true); }); + + /* + INTERFACE THEME SWITCH MUST NOT CORRUPT CONTENT DARK MODE'S OWN CELL COLORS + + GlobalSkin (the interface skin's live color object) is a direct reference + into EditorSkins["theme-light"]/["theme-dark"], not an independent copy. + Content dark mode's cell background/grid colors must stay at their own + fixed values regardless of which interface skin is active. + */ + test('switching interface theme does not change content dark mode\'s cell colors', async ({ page }) => { + const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); + + const readCellColors = () => + frameEval(editorPage, (win) => { + const dark = (win as any).AscCommon.EditorSkins['theme-dark']; + return { background: dark.CellBackground, grid: dark.CellGrid }; + }); + + // TODO: uses the same internal-API shortcut as the test above + // (Common.UI.Themes.setTheme) rather than clicking the actual + // interface-theme picker in the UI -- verifies the color values + // themselves stay correct, not that the real UI control reaches this + // code path. A fuller e2e would drive the actual theme-switcher. + await frameEval(editorPage, (win) => win.Common.UI.Themes.setTheme('theme-contrast-dark')); + + expect(await readCellColors()).toEqual({ background: '#262626', grid: '#454545' }); + }); }); diff --git a/e2e/tests/spreadsheeteditor/darkmode/page-break-preview.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/page-break-preview.spec.ts index 3849f94219..88bbd4dd4e 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/page-break-preview.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/page-break-preview.spec.ts @@ -5,16 +5,10 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { /* PAGE BREAK PREVIEW, DARK MODE: OUTSIDE-PRINT-AREA OVERLAY MUST MATCH THE GRID-LINE COLOR - Page Break Preview shades the area outside the print range using - cells.defaultState.border -- the same theme-resolved color every ordinary - grid line already uses (drawn via setStrokeStyle, never dark-mode - corrected). The overlay itself is painted through drawFillCell instead, - which re-applies getDarkModeCorrectedColor unless bIsExplicitFill says - otherwise. Before the fix, that flag came out false in the main - _drawRowBG loop, and wasn't passed at all for merged-cell sub-rectangles - -- so the already-resolved border color got corrected a second time, - landing a near-white overlay next to dark-grey grid lines in dark mode. - See TASKS/10-build-pr-endsession-sanitizer for the diagnosis. + The overlay outside the print range must use the same theme-resolved + border color as ordinary grid lines. A regression that corrects this + color a second time would paint a near-white overlay next to dark-grey + grid lines instead of blending into them. */ test('page-break-preview overlay color matches the grid-line border color in dark mode', async ({ page }) => { const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); diff --git a/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts index 1c150d9c55..ca73e8301b 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts @@ -89,16 +89,10 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { /* PATTERN-FILLED CELL, EDITED IN DARK MODE: TEXT MUST STAY READABLE - Unlike a gradient fill, a genuine pattern fill (patternType other than - None/Solid) makes Fill.prototype.bg() return non-null -- the pattern's - foreground color -- and that's exactly what the editor paints as a flat - background (background: bg || defaultState.background), raw, with no - dark-mode correction of its own. Before the fix, the automatic-text-color - decision only ever got a resolvedFallbackBg when bg was null, so a - pattern fill fell into the grid path's "unknown contrast" exemption even - though the actual painted color was fully known right there -- text - rendered at its literal stored black regardless of how dark the - pattern's foreground actually was. + Unlike a gradient fill, a genuine pattern fill has a known foreground + color, and that's exactly what the editor paints as the background. + Automatic text must be corrected for contrast against that color like + any other known background, not treated as an unknown-contrast case. */ test('editing a pattern-filled cell keeps automatic text readable against its foreground color', async ({ page }) => { const { editorPage } = await openNewEditor(page, 'a.try-editor.cell', /\.xlsx/); From 933c5d397219a014b503db0b55bf0da9cf4bc8b1 Mon Sep 17 00:00:00 2001 From: j-base64 Date: Fri, 31 Jul 2026 17:28:12 +0200 Subject: [PATCH 8/8] refactor(e2e): extract ceColors sampling helper shared by three dark-mode tests Signed-off-by: j-base64 Assisted-by: ClaudeCode:claude-sonnet-5 --- .../darkmode/mid-edit-toggle.spec.ts | 20 +++--------- ...tern-and-gradient-fill-cell-editor.spec.ts | 32 ++++--------------- e2e/tests/utils/spreadsheet-editor.ts | 14 ++++++++ 3 files changed, 25 insertions(+), 41 deletions(-) diff --git a/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.spec.ts index 38975b0439..879b5bf4a2 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/mid-edit-toggle.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test'; -import { openNewEditor, editorApi, frameEval } from '../../helpers'; -import { sampleCellPixels } from '../../utils/spreadsheet-editor'; +import { openNewEditor, editorApi } from '../../helpers'; +import { sampleCellPixels, ceColors } from '../../utils/spreadsheet-editor'; test.describe('Spreadsheet editor - dark mode rendering', () => { /* @@ -18,17 +18,7 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { await editorPage.keyboard.press('Control+Home'); await editorPage.keyboard.type('hello'); - const ceColors = () => - frameEval(editorPage, (win) => { - const canvas = win.document.getElementById('ce-canvas') as HTMLCanvasElement; - const ctx = canvas.getContext('2d')!; - const img = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - const set = new Set(); - for (let i = 0; i < img.length; i += 4) set.add(img[i] + ',' + img[i + 1] + ',' + img[i + 2]); - return Array.from(set).sort(); - }); - - const before = await ceColors(); + const before = await ceColors(editorPage); await editorApi(editorPage, (api) => api.asc_setContentDarkMode(true)); // Confirm the toggle is genuinely in effect elsewhere (the main grid), @@ -36,13 +26,13 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { // toggle simply not having landed yet. await expect.poll(() => sampleCellPixels(editorPage, 5, 5).then((s) => s.darkest)).not.toEqual([255, 255, 255]); - const after = await ceColors(); + const after = await ceColors(editorPage); expect(after).toEqual(before); // ENDING THE EDIT AND STARTING A NEW ONE DOES PICK UP THE NEW THEME await editorPage.keyboard.press('Escape'); await editorPage.keyboard.press('Control+Home'); await editorPage.keyboard.type('world'); - await expect.poll(async () => (await ceColors()).includes('38,38,38')).toBe(true); + await expect.poll(async () => (await ceColors(editorPage)).includes('38,38,38')).toBe(true); }); }); diff --git a/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts b/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts index ca73e8301b..82f30e7c59 100644 --- a/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts +++ b/e2e/tests/spreadsheeteditor/darkmode/pattern-and-gradient-fill-cell-editor.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test'; -import { openNewEditor, editorApi, frameEval } from '../../helpers'; -import { cellFillRgb, cellHasGradientFill } from '../../utils/spreadsheet-editor'; +import { openNewEditor, editorApi } from '../../helpers'; +import { cellFillRgb, cellHasGradientFill, ceColors } from '../../utils/spreadsheet-editor'; test.describe('Spreadsheet editor - dark mode rendering', () => { /* @@ -46,20 +46,10 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { }); await expect.poll(() => cellHasGradientFill(editorPage, 1, 1)).toBe(true); - const ceColors = () => - frameEval(editorPage, (win) => { - const canvas = win.document.getElementById('ce-canvas') as HTMLCanvasElement; - const ctx = canvas.getContext('2d')!; - const img = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - const set = new Set(); - for (let i = 0; i < img.length; i += 4) set.add(img[i] + ',' + img[i + 1] + ',' + img[i + 2]); - return Array.from(set).sort(); - }); - // START EDITING B2 IN LIGHT MODE -- ALREADY-WORKING BASELINE await editorPage.keyboard.press('F2'); - const lightColors = await ceColors(); + const lightColors = await ceColors(editorPage); // Pre-existing, unrelated limitation: the editor shows a plain white // background, not the actual gradient -- confirmed here as the baseline // this test builds on, not something this fix changes. @@ -74,7 +64,7 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { await editorApi(editorPage, (api) => api.asc_findCell('B2')); await editorPage.keyboard.press('F2'); - const darkColors = await ceColors(); + const darkColors = await ceColors(editorPage); // The editor's fallback background is cells.defaultState.background's // dark-mode value (#262626 = 38,38,38) -- confirms the fallback is in // effect, same as the no-fill case already covered by cell-colors.spec.ts. @@ -119,20 +109,10 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { }); await expect.poll(() => cellFillRgb(editorPage, 2, 1)).not.toBeNull(); - const ceColors = () => - frameEval(editorPage, (win) => { - const canvas = win.document.getElementById('ce-canvas') as HTMLCanvasElement; - const ctx = canvas.getContext('2d')!; - const img = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - const set = new Set(); - for (let i = 0; i < img.length; i += 4) set.add(img[i] + ',' + img[i + 1] + ',' + img[i + 2]); - return Array.from(set).sort(); - }); - // START EDITING B3 IN LIGHT MODE -- ALREADY-WORKING BASELINE await editorPage.keyboard.press('F2'); - const lightColors = await ceColors(); + const lightColors = await ceColors(editorPage); // The editor paints the pattern's foreground color flat, raw -- not // corrected in either mode, confirmed here as the baseline. expect(lightColors).toContain('30,30,90'); @@ -145,7 +125,7 @@ test.describe('Spreadsheet editor - dark mode rendering', () => { await editorApi(editorPage, (api) => api.asc_findCell('B3')); await editorPage.keyboard.press('F2'); - const darkColors = await ceColors(); + const darkColors = await ceColors(editorPage); // Same foreground color as before -- the editor's background paint is // mode-independent, unaffected by this fix. expect(darkColors).toContain('30,30,90'); diff --git a/e2e/tests/utils/spreadsheet-editor.ts b/e2e/tests/utils/spreadsheet-editor.ts index 35759c53f5..594bd6ee7b 100644 --- a/e2e/tests/utils/spreadsheet-editor.ts +++ b/e2e/tests/utils/spreadsheet-editor.ts @@ -166,3 +166,17 @@ export function resizeColumnGuideColor(editorPage: Page, col: number) { return null; }, [col]); } + +// Every distinct RGB triple currently painted on the cell editor's own canvas, +// sorted -- used to assert the editor's rendered colors change (or don't) +// across a dark-mode toggle, independent of layout. +export function ceColors(editorPage: Page) { + return frameEval(editorPage, (win) => { + const canvas = win.document.getElementById('ce-canvas') as HTMLCanvasElement; + const ctx = canvas.getContext('2d')!; + const img = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + const set = new Set(); + for (let i = 0; i < img.length; i += 4) set.add(img[i] + ',' + img[i + 1] + ',' + img[i + 2]); + return Array.from(set).sort(); + }); +}